aboutsummaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorSergey Poznyakoff <gray@gnu.org.ua>2017-09-13 15:13:34 +0200
committerSergey Poznyakoff <gray@gnu.org.ua>2017-09-13 15:23:44 +0200
commit6599db656e097d8eb22921d2c2ce3451c8147563 (patch)
tree2b04a44ba67b088e0c62ac364c75b72ef928b8a9 /lib
parent36f66056a4ddbba8f300ef2fd15955e292e2755f (diff)
downloadacmeman-6599db656e097d8eb22921d2c2ce3451c8147563.tar.gz
acmeman-6599db656e097d8eb22921d2c2ce3451c8147563.tar.bz2
Rewrite
This patch introduces acmeman configuration file, which can be used to direct its action if a server other than Apache is used. It also can be instructed to store certificate, certificate chain, and certificate key in a single file, instead of three different ones. This can be used with such servers as pound(8). In the absense of a configuration file, the program operates as in previous versions. * MANIFEST: Update. * Makefile.PL: Update. * Changes: Update. * acmeman: Use configuration file if present. Apache configuration remains as a default source of TLS domains. Configuration file can override or complement it. * lib/App/Acmeman/Domain.pm: New file. * lib/App/Acmeman/Config.pm: New file. * lib/App/Acmeman/Source/Apache.pm: New file. * lib/App/Acmeman/Apache/Layout.pm: New file.
Diffstat (limited to 'lib')
-rw-r--r--lib/App/Acmeman/Apache/Layout.pm117
-rw-r--r--lib/App/Acmeman/Config.pm359
-rw-r--r--lib/App/Acmeman/Domain.pm138
-rw-r--r--lib/App/Acmeman/Source/Apache.pm284
4 files changed, 898 insertions, 0 deletions
diff --git a/lib/App/Acmeman/Apache/Layout.pm b/lib/App/Acmeman/Apache/Layout.pm
new file mode 100644
index 0000000..77e2f8f
--- /dev/null
+++ b/lib/App/Acmeman/Apache/Layout.pm
@@ -0,0 +1,117 @@
+package App::Acmeman::Apache::Layout;
+
+use strict;
+use warnings;
+use Carp;
+use File::Basename;
+
+require Exporter;
+our @ISA = qw(Exporter);
+
+my %apache_layout_tab = (
+ slackware => {
+ _config_file => '/etc/httpd/httpd.conf',
+ _incdir => '/etc/httpd/extra',
+ _restart => '/etc/rc.d/rc.httpd restart'
+ },
+ debian => {
+ _config_file => '/etc/apache2/apache2.conf',
+ _incdir => sub {
+ for my $dir ('/etc/apache2/conf-available',
+ '/etc/apache2/conf.d') {
+ return $dir if -d $dir;
+ }
+ carp 'none of the expected configuration directories found; falling back to /etc/apache2';
+ return '/etc/apache2';
+ },
+ _restart => 'service apache2 restart',
+ _post_setup => sub {
+ my ($filename) = @_;
+ my $dir = dirname($filename);
+ my $name = basename($filename);
+ if ($dir eq '/etc/apache2/conf-available') {
+ chdir('/etc/apache2/conf-enabled');
+ symlink "../conf-available/$name", $name;
+ }
+ }
+ },
+ rh => {
+ _config_file => '/etc/httpd/conf/httpd.conf',
+ _incdir => '/etc/httpd/conf.d',
+ _restart => 'service httpd restart'
+ },
+ suse => {
+ _config_file => '/etc/apache2/httpd.conf',
+ _incdir => '/etc/apache2/conf.d',
+ _restart => 'service httpd restart'
+ # or systemctl restart apache2.service
+ }
+);
+
+# new(NAME)
+# new()
+sub new {
+ my $class = shift;
+ my $self = bless { }, $class;
+ my $name;
+
+ if (@_ == 0) {
+ # Autodetect
+ while (my ($n, $layout) = each %apache_layout_tab) {
+ if (-f $layout->{_config_file}) {
+ debug(2, "assuming Apache layout \"$n\"");
+ $name = $n;
+ last;
+ }
+ }
+ croak "unrecognized Apache layout" unless defined $name;
+ } elsif (@_ == 1) {
+ $name = shift;
+ }
+
+ if (exists($apache_layout_tab{$name})) {
+ @{$self}{keys %{$apache_layout_tab{$name}}} =
+ values %{$apache_layout_tab{$name}};
+ } else {
+ croak "undefined Apache layout $name";
+ }
+
+ $self->{_layout_name} = $name;
+
+ return $self;
+}
+
+sub debug {
+ if (defined(&::debug)) {
+ ::debug(@_);
+ }
+}
+
+sub name {
+ my $self = shift;
+ return $self->{_layout_name};
+}
+
+sub config_file {
+ my $self = shift;
+ return $self->{_config_file};
+}
+
+sub restart_command {
+ my $self = shift;
+ return $self->{_restart};
+}
+
+sub incdir {
+ my $self = shift;
+ if (exists($self->{_incdir})) {
+ if (ref($self->{_incdir}) eq 'CODE') {
+ return &{$self->{_incdir}};
+ } else {
+ return $self->{_incdir};
+ }
+ }
+ return dirname($self->{_config_file});
+}
+
+1;
diff --git a/lib/App/Acmeman/Config.pm b/lib/App/Acmeman/Config.pm
new file mode 100644
index 0000000..bbe1f2b
--- /dev/null
+++ b/lib/App/Acmeman/Config.pm
@@ -0,0 +1,359 @@
+package App::Acmeman::Config;
+
+use strict;
+use warnings;
+use Carp;
+
+require Exporter;
+our @ISA = qw(Exporter);
+our $VERSION = "1.00";
+
+sub new {
+ my ($class, $filename, %args) = @_;
+ my $self = bless { _filename => $filename }, $class;
+ my $defaults;
+ my $v;
+
+ if (defined($v = delete $args{syntax})) {
+ $self->{_syntax} = $v;
+ }
+ $defaults = delete $args{defaults};
+
+ carp "unrecognized parameters" if keys(%args);
+ $self->{_conf} = {};
+ if (-e $filename) {
+ $self->_readconfig($filename, $self->{_conf});
+ } elsif ($defaults) {
+ while (my ($k, $v) = each %$defaults) {
+ $self->set(split(/\./, $k), $v);
+ }
+ } else {
+ $self->error("configuration file \"$filename\" does not exist");
+ }
+
+ if ($self->success && exists($self->{_syntax})) {
+ $self->_fixup($self->{_syntax}, $self->{_conf});
+ }
+ return $self;
+}
+
+sub error {
+ my ($self, $msg) = @_;
+ push @{$self->{_errors}}, $msg;
+}
+
+sub errors {
+ my $self = shift;
+ if (wantarray) {
+ return exists($self->{_errors}) ? (@{$self->{_errors}}) : ();
+ } elsif (!exists($self->{_errors})) {
+ return 0;
+ } else {
+ return 0 + @{$self->{_errors}};
+ }
+}
+
+sub clrerr {
+ my $self = shift;
+ delete $self->{_errors};
+}
+
+sub success {
+ my $self = shift;
+ return $self->errors == 0;
+}
+
+sub _parse_section {
+ my ($self, $conf, $input) = @_;
+ my $quote;
+ my $kw = $self->{_syntax} if exists $self->{_syntax};
+ while ($input ne '') {
+ my $name;
+ if (!defined($quote)) {
+ if ($input =~ /^"(.*)/) {
+ $quote = '';
+ $input = $1;
+ } elsif ($input =~ /^(.+?)(?:\s+|")(.*)/) {
+ $name = $1;
+ $input = $2;
+ } else {
+ $name = $input;
+ $input = '';
+ }
+ } else {
+ if ($input =~ /^([^\\"]*)\\(.)(.*)/) {
+ $quote .= $1 . $2;
+ $input = $3;
+ } elsif ($input =~ /^([^\\"]*)"\s*(.*)/) {
+ $name = $quote . $1;
+ $input = $2;
+ $quote = undef;
+ } else {
+ error("unparsable input $input");
+ exit(2);
+ }
+ }
+
+ if (defined($name)) {
+ $conf->{$name} = {} unless ref($conf->{$name}) eq 'HASH';
+ $conf = $conf->{$name};
+ if (defined($kw) and ref($kw) eq 'HASH') {
+ my $synt;
+ if (exists($kw->{$name})) {
+ $synt = $kw->{$name};
+ } elsif (exists($kw->{'*'})) {
+ $synt = $kw->{'*'};
+ if ($synt eq '*') {
+ $name = undef;
+ next;
+ }
+ }
+ if (defined($synt)
+ && ref($synt) eq 'HASH'
+ && exists($synt->{section})) {
+ $kw = $synt->{section};
+ } else {
+ $kw = undef;
+ }
+ } else {
+ $kw = undef;
+ }
+ $name = undef;
+ }
+ }
+ return ($conf, $kw);
+}
+
+sub _readconfig {
+ my ($self, $file, $conf) = @_;
+
+# debug(2, "reading $file");
+ open(my $fd, "<", $file)
+ or do {
+ $self->error("can't open configuration file $file: $!");
+ return;
+ };
+
+ my $line;
+ my $err;
+ my $section = $conf;
+ my $kw = $self->{_syntax};
+ my $include = 0;
+ my $rootname;
+
+ while (<$fd>) {
+ ++$line;
+ chomp;
+ if (/\\$/) {
+ chop;
+ $_ .= <$fd>;
+ redo;
+ }
+
+ s/^\s+//;
+ s/\s+$//;
+ s/#.*//;
+ next if ($_ eq "");
+
+ if (/^\[(.+?)\]$/) {
+ $include = 0;
+ my $arg = $1;
+ $arg =~ s/^\s+//;
+ $arg =~ s/\s+$//;
+ if ($arg eq 'include') {
+ $include = 1;
+ } else {
+ ($section, $kw) = $self->_parse_section($conf, $1);
+ if (exists($self->{_syntax}) && !defined($kw)) {
+ $self->error("$file:$line: unknown section");
+ }
+ }
+ } elsif (/([\w_-]+)\s*=\s*(.*)/) {
+ my ($k, $v) = ($1, $2);
+
+ if ($include) {
+ if ($k eq 'path') {
+ $self->_readconfig($v, $conf);
+ } elsif ($k eq 'pathopt') {
+ $self->_readconfig($v, $conf)
+ if -f $v;
+ } elsif ($k eq 'glob') {
+ foreach my $file (bsd_glob($v, 0)) {
+ $self->_readconfig($file, $conf);
+ }
+ } else {
+ $self->error("$file:$line: unknown keyword");
+ }
+ next;
+ }
+
+ if (defined($kw)) {
+ my $x = $kw->{$k};
+ if (!defined($x)) {
+ $self->error("$file:$line: unknown keyword $k");
+ next;
+ } elsif (ref($x) eq 'HASH') {
+ if (exists($x->{re})) {
+ if ($v !~ /$x->{re}/) {
+ $self->error("$file:$line: invalid value for $k");
+ next;
+ }
+ }
+
+ if (exists($x->{check})) {
+ if (my $errstr = &{$x->{check}}($k, $v)) {
+ $self->error("$file:$line: $errstr");
+ next;
+ }
+ }
+
+ if (exists($x->{parser})) {
+ if (my $errstr = &{$x->{parser}}($k, \$v)) {
+ $self->error("$file:$line: $errstr");
+ next;
+ }
+ }
+
+ if ($x->{array}) {
+ if (exists($section->{$k})) {
+ $v = [ @{$section->{$k}}, $v ];
+ } else {
+ $v = [ $v ]
+ }
+ }
+ }
+ }
+
+ $section->{$k} = $v;
+ } else {
+ $self->error("$file:$line: malformed line");
+ next;
+ }
+ }
+ close $fd;
+}
+
+sub _is_section_ref {
+ my ($ref) = @_;
+ return ref($ref) eq 'HASH';
+}
+
+sub _fixup {
+ my ($self, $kw, $section, @path) = @_;
+
+ while (my ($k, $d) = each %{$kw}) {
+ if (ref($d) eq 'HASH') {
+ if (exists($d->{default}) && !exists($section->{$k})) {
+ $section->{$k} = $d->{default};
+ }
+
+ if (exists($d->{section})) {
+ if ($k eq '*') {
+ while (my ($name, $vref) = each %{$section}) {
+ if (_is_section_ref($vref)) {
+ $self->_fixup($d->{section}, $vref, @_, $name);
+ }
+ }
+ } else {
+ my $temp;
+ unless (exists $section->{$k}) {
+ $section->{$k} = {} ;
+ $temp = 1;
+ }
+ $self->_fixup($d->{section}, $section->{$k}, @_, $k);
+ delete $section->{$k}
+ if $temp && keys(%{$section->{$k}}) == 0;
+ }
+ }
+
+ if ($d->{mandatory} && !exists($section->{$k})) {
+ $self->error(exists($d->{section})
+ ? "mandatory section ["
+ . join(' ', @_, $k)
+ . "] not present"
+ : "mandatory variable \""
+ . join('.', @_, $k)
+ . "\" not set");
+ }
+ }
+ }
+}
+
+sub _getref {
+ my $self = shift;
+
+ return undef unless exists $self->{_conf};
+ my $ref = $self->{_conf};
+ for (@_) {
+ carp "component undefined" unless defined $_;
+ return undef unless exists $ref->{$_};
+ $ref = $ref->{$_};
+ }
+ return $ref;
+}
+
+sub get {
+ my $self = shift;
+ if (my $ref = $self->_getref(@_)) {
+ if (ref($ref) eq 'ARRAY') {
+ if (wantarray) {
+ return (@{$ref});
+ } else {
+ return $ref->[0];
+ }
+ }
+ return $ref;
+ }
+}
+
+sub isset {
+ my $self = shift;
+ return defined $self->_getref(@_);
+}
+
+sub set {
+ my $self = shift;
+ $self->{_conf} = {} unless exists $self->{_conf};
+ my $ref = $self->{_conf};
+ my $synt = $self->{_syntax};
+
+# print "SET ".join('.',@_)."\n";
+ while (my $arg = shift) {
+ if ($synt) {
+ if (exists($synt->{$arg})) {
+ $synt = $synt->{$arg};
+ } elsif (exists($synt->{'*'})) {
+ $synt = $synt->{'*'};
+ } else {
+ croak "no such component in syntax: $arg";
+ }
+ }
+
+ if (@_ == 1) {
+ my $v = shift;
+ if ($synt && ref($synt) eq 'HASH') {
+ if ($synt->{array}) {
+ if (exists($ref->{$arg})) {
+ $v = [ @{$ref->{$arg}}, $v ];
+ } else {
+ $v = [ $v ];
+ }
+ }
+ }
+ $ref->{$arg} = $v;
+ return;
+ }
+
+ if ($synt) {
+ croak "component not a section: $arg"
+ unless $synt->{section};
+ $synt = $synt->{section};
+ }
+ $ref->{$arg} = {} unless exists $ref->{$arg};
+ $ref = $ref->{$arg};
+ }
+}
+
+1;
+
+
diff --git a/lib/App/Acmeman/Domain.pm b/lib/App/Acmeman/Domain.pm
new file mode 100644
index 0000000..64d4275
--- /dev/null
+++ b/lib/App/Acmeman/Domain.pm
@@ -0,0 +1,138 @@
+package App::Acmeman::Domain;
+
+use strict;
+use warnings;
+use Carp;
+
+require Exporter;
+our @ISA = qw(Exporter);
+our @EXPORT_OK = qw(CERT_FILE KEY_FILE CA_FILE);
+our %EXPORT_TAGS = ( files => [ qw(CERT_FILE KEY_FILE CA_FILE) ] );
+our $VERSION = '1.00';
+
+use constant {
+ CERT_FILE => 0,
+ KEY_FILE => 1,
+ CA_FILE => 2
+};
+
+sub uniq {
+ my %h;
+ map {
+ if (exists($h{$_})) {
+ ()
+ } else {
+ $h{$_} = 1;
+ $_
+ }
+ } @_;
+}
+
+sub new {
+ my ($class, %args) = @_;
+
+ my $self = bless { }, $class;
+ my $v;
+
+ if ($v = delete $args{cn}) {
+ $self->{_cn} = lc $v;
+ }
+ if ($v = delete $args{alt}) {
+ if (ref($v) eq 'ARRAY') {
+ $self->{_alt} = [ uniq(map { lc } @$v) ];
+ } else {
+ $self->{_alt} = [ lc $v ];
+ }
+ }
+ if ($v = delete $args{type}) {
+ $self->{_cert_type} = $v;
+ }
+ if (($v = delete $args{certificate_file})
+ || ($v = delete $args{'certificate-file'})) {
+ $self->{_file}[CERT_FILE] = $v;
+ }
+ if (($v = delete $args{ca_file})
+ || ($v = delete $args{'ca-file'})) {
+ $self->{_file}[CA_FILE] = $v;
+ }
+ if (($v = delete $args{key_file})
+ || ($v = delete $args{'key-file'})) {
+ $self->{_file}[KEY_FILE] = $v;
+ }
+
+ $v = delete($args{argument}) || '$domain';
+ $v =~ s{\$}{\\\$};
+ $self->{_argument} = qr($v);
+
+ croak "unrecognized arguments" if keys %args;
+ return $self;
+}
+
+sub names {
+ my $self = shift;
+ if (wantarray) {
+ return ($self->cn, $self->alt);
+ } else {
+ return $self->alt() + 1;
+ }
+}
+
+sub contains {
+ my $self = shift;
+ my $val = lc shift;
+ return grep { $_ eq $val } $self->names;
+}
+
+sub _domain_cmp {
+ my ($a,$b) = @_;
+
+ carp "righthand-side argument should be a App::Acmeman::Domain"
+ unless $b->isa('App::Acmeman::Domain');
+ return $a->{_cn} cmp $b->{_cn};
+}
+
+sub _domain_plus {
+ my ($a, $b) = @_;
+
+ carp "righthand-side argument should be a App::Acmeman::Domain"
+ unless $b->isa('App::Acmeman::Domain');
+
+ push @{$a->{_alt}}, $b->cn
+ unless $a->contains($b->cn);
+ @{$a->{_alt}} = uniq($a->alt, $b->alt);
+}
+
+use overload
+ cmp => \&_domain_cmp,
+ '+' => \&domain_plus,
+ '""' => sub { $_[0]->cn };
+
+sub cn {
+ my $self = shift;
+ return $self->{_cn};
+}
+
+sub alt {
+ my $self = shift;
+ if (wantarray) {
+ return exists($self->{_alt}) ? (@{$self->{_alt}}) : ();
+ } else {
+ return exists($self->{_alt}) ? (0+@{$self->{_alt}}) : 0;
+ }
+}
+
+sub file {
+ my ($self, $type) = @_;
+ return undef unless exists($self->{_file}[$type]);
+ my $res = $self->{_file}[$type];
+ $res =~ s{$self->{_argument}}{$self->cn}ge;
+ return $res;
+}
+
+sub certificate_file {
+ my $self = shift;
+ return $self->file(CERT_FILE);
+}
+
+1;
+
diff --git a/lib/App/Acmeman/Source/Apache.pm b/lib/App/Acmeman/Source/Apache.pm
new file mode 100644
index 0000000..f86c02f
--- /dev/null
+++ b/lib/App/Acmeman/Source/Apache.pm
@@ -0,0 +1,284 @@
+package App::Acmeman::Source::Apache;
+
+use strict;
+use warnings;
+use Carp;
+use feature 'state';
+use File::Path qw(make_path);
+
+require App::Acmeman::Apache::Layout;
+our @ISA = qw(App::Acmeman::Apache::Layout);
+
+sub new {
+ my $class = shift;
+ my $self = $class->SUPER::new(@_);
+ return $self;
+}
+
+sub debug {
+ if (defined(&::debug)) {
+ ::debug(@_);
+ }
+}
+
+sub configure {
+ my ($self, $config) = @_;
+ $config->set(qw(core restart), $self->restart_command);
+ $self->{_cfg} = $config;
+ return $self->examine_http_config($self->config_file);
+}
+
+sub set {
+ my $self = shift;
+ croak "improper use of the set method"
+ unless exists $self->{_cfg};
+ return $self->{_cfg}->set(@_);
+}
+
+sub get {
+ my $self = shift;
+ croak "improper use of the get method"
+ unless exists $self->{_cfg};
+ return $self->{_cfg}->get(@_);
+}
+
+sub error {
+ my $self = shift;
+ if (exists($self->{_cfg})) {
+ $self->{_cfg}->error(@_);
+ } else {
+ carp @_;
+ }
+}
+
+sub dequote {
+ my ($self, $arg) = @_;
+ $arg =~ s{^"(.*?)"$}{$1};
+ return $arg;
+}
+
+sub examine_http_config {
+ my ($self, $file) = @_;
+
+ use constant {
+ STATE_INITIAL => 0, # Initial state
+ STATE_VIRTUAL_HOST => 1, # In VirtualHost block
+ STATE_USE_CHALLENGE => 2, # Ditto, but challenge macro was used
+ STATE_USE_REFERENCE => 3, # Ditto, but reference macro was used
+ STATE_MACRO_CHALLENGE => 4, # In LetsEncryptChallenge macro
+ STATE_MACRO_SSL => 5
+ };
+
+ state $state = STATE_INITIAL;
+
+ debug(3, "reading apache configuration file \"$file\"");
+ if (open(my $fd, '<', $file)) {
+ my $server_name;
+ my @server_aliases;
+ my $reference;
+ my $line;
+
+ while (<$fd>) {
+ ++$line;
+ chomp;
+ s/^\s+//;
+ next if /^(#.*)?$/;
+ if (/^include(optional)?\s+(.+?)\s*$/i) {
+# debug(3, "$file:$line: state $state: Include$1 $2");
+ $self->http_include($self->dequote($2), defined($1));
+ next;
+ }
+
+ if ($state == STATE_INITIAL) {
+ if (/^<VirtualHost/i) {
+ $state = STATE_VIRTUAL_HOST;
+ $server_name = undef;
+ @server_aliases = ();
+ $reference = undef;
+ } elsif (/^ServerRoot\s+(.+)/i) {
+ $self->{_server_root} = $self->dequote($1);
+ } elsif (/^<(?:(?i)Macro)\s+LetsEncryptChallenge/) {
+ $state = STATE_MACRO_CHALLENGE;
+ } elsif (/^<(?:(?i)Macro)\s+LetsEncryptSSL\s+(.+?)\s*>/) {
+ $state = STATE_MACRO_SSL;
+ $self->set(qw(files apache argument), $1);
+ }
+ } elsif ($state == STATE_VIRTUAL_HOST
+ || $state == STATE_USE_CHALLENGE
+ || $state == STATE_USE_REFERENCE) {
+ if (m{</VirtualHost}i) {
+ unshift @server_aliases, $server_name
+ if defined $server_name;
+ if (@server_aliases) {
+ if ($state == STATE_USE_CHALLENGE) {
+ my $cn = shift @server_aliases;
+ $self->set('domain', $cn, 'files', 'apache');
+ foreach my $name (@server_aliases) {
+ $self->set('domain', $cn, 'alt', $name);
+ }
+ debug(3, "$file:$line: will handle ".
+ join(',', $cn, @server_aliases));
+ } elsif ($state == STATE_USE_REFERENCE) {
+ $self->set('domain', $reference, 'files', 'apache');
+ foreach my $name (@server_aliases) {
+ $self->set('domain', $reference,
+ 'alt', $name);
+ }
+ }
+ }
+ $state = STATE_INITIAL;
+ } elsif (/^(?:(?i)Use)\s+LetsEncryptChallenge/) {
+ if ($state == STATE_VIRTUAL_HOST) {
+ $state = STATE_USE_CHALLENGE;
+ } elsif ($state == STATE_USE_CHALLENGE) {
+ $self->error("$file:$line: duplicate use of LetsEncryptChallenge");
+ } else {
+ $self->error("$file:$line: LetsEncryptChallenge and LetsEncryptReference can't be used together");
+ }
+ } elsif (/^(?:(?i)Use)\s+LetsEncryptReference\s+(.+)\s*$/) {
+ if ($state == STATE_VIRTUAL_HOST) {
+ $state = STATE_USE_REFERENCE;
+ $reference = $1;
+ } elsif ($state == STATE_USE_REFERENCE) {
+ $self->error("$file:$line: duplicate use of LetsEncryptReference");
+ } else {
+ $self->error("$file:$line: LetsEncryptChallenge and LetsEncryptReference can't be used together");
+ }
+ } elsif (/^(?:(?i)ServerName)\s+(\S+)/) {
+ $server_name = $self->dequote($1);
+ } elsif (/^(?:(?i)ServerAlias)\s+(.+)\s*$/) {
+ push @server_aliases,
+ map { /^\*\./ ? () : $self->dequote($_) }
+ split /\s+/, $1;
+ }
+ } elsif ($state == STATE_MACRO_CHALLENGE) {
+ if (m{^</macro}i) {
+ $state = STATE_INITIAL;
+ } elsif (m{^(?:(?i)Alias)\s+/.well-known/acme-challenge\s+(.+)}) {
+ my $dir = $self->dequote($1);
+ $dir =~ s{/.well-known/acme-challenge$}{};
+ $self->set(qw(core rootdir), $dir);
+ debug(3, "ACME challenge root dir: $dir");
+ }
+ } elsif ($state == STATE_MACRO_SSL) {
+ if (m{^</macro}i) {
+ $state = STATE_INITIAL;
+ } elsif (/(?:(?i)SSLCertificate((?:Key)|(?:Chain))?File)\s+(.+)/) {
+ my %t = (
+ '' => 'certificate-file',
+ key => 'key-file',
+ chain => 'ca-file'
+ );
+ $self->set(qw(files apache), $t{lc($1||'')}, $2);
+ }
+ }
+ }
+ close $fd;
+ } else {
+ $self->error("can't open file \"$file\": $!");
+ return 0;
+ }
+ return 1;
+}
+
+sub http_include {
+ my ($self, $pattern, $optional) = @_;
+ $pattern = "$self->{_server_root}/$pattern" unless $pattern =~ m{^/};
+ $pattern =~ s{/*$}{};
+ $pattern .= '/*' if -d $pattern;
+ foreach my $file (glob $pattern) {
+ if ($optional && ! -e $file) {
+ debug(1, "optional include file \"$file\" doesn't exist");
+ next;
+ }
+ $self->examine_http_config($file);
+ }
+}
+
+sub mkpath {
+ my ($self, $dir) = @_;
+ my @created = make_path("$dir", { error => \my $err } );
+ if (@$err) {
+ for my $diag (@$err) {
+ my ($file, $message) = %$diag;
+ if ($file eq '') {
+ $self->error($message);
+ } else {
+ $self->error("mkdir $file: $message");
+ }
+ }
+ return 0;
+ }
+ return 1;
+}
+
+sub setup {
+ my ($self, %args) = @_;
+ my $filename = $self->incdir() . "/httpd-letsencrypt.conf";
+ if (-e $filename) {
+ if ($args{force}) {
+ ::error("the file \"$filename\" already exists",
+ prefix => 'warning');
+ } else {
+ ::error("the file \"$filename\" already exists");
+ ::error("use --force to continue");
+ return 0;
+ }
+ }
+ my $www_root = $self->get(qw(core rootdir));
+ debug(2, "writing $filename");
+ unless ($args{dry_run}) {
+ unless ($self->mkpath($self->incdir())) {
+ return 0;
+ }
+ open(my $fd, '>', $filename)
+ or croak "can't open \"$filename\" for writing: $!";
+ print $fd <<EOT;
+<Macro LetsEncryptChallenge>
+ Alias /.well-known/acme-challenge $www_root/.well-known/acme-challenge
+ <Directory $www_root/.well-known/acme-challenge>
+ Options None
+ Require all granted
+ </Directory>
+ <IfModule mod_rewrite.c>
+ RewriteEngine On
+ RewriteRule /.well-known/acme-challenge - [L]
+ </IfModule>
+</Macro>
+
+<Macro LetsEncryptReference \$domain>
+ Use LetsEncryptChallenge
+ Alias /.dummy/\$domain /dev/null
+</Macro>
+
+<Macro LetsEncryptSSL \$domain>
+ SSLEngine on
+ SSLProtocol all -SSLv2 -SSLv3
+ SSLHonorCipherOrder on
+ SSLCipherSuite EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:EECDH+RC4:RSA+RC4:!MD5
+ SSLCertificateFile /etc/ssl/acme/\$domain/cert.pem
+ SSLCertificateKeyFile /etc/ssl/acme/\$domain/privkey.pem
+ SSLCACertificateFile /etc/ssl/acme/lets-encrypt-x3-cross-signed.pem
+</Macro>
+
+<Macro LetsEncryptServer \$domain>
+ ServerName \$domain
+ Use LetsEncryptSSL \$domain
+</Macro>
+
+EOT
+;
+ close $fd;
+
+ if (exists($self->{_post_setup})) {
+ &{$self->{_post_setup}}($filename);
+ }
+ }
+
+ ::error("created file \"$filename\"", prefix => 'note');
+ ::error("please, enable mod_macro and make sure your Apache configuration includes this file", prefix => 'note');
+
+ return 1;
+}
+
+1;

Return to:

Send suggestions and report system problems to the System administrator.