aboutsummaryrefslogtreecommitdiff
path: root/lib/App/Acmeman/Source/Apache.pm
diff options
context:
space:
mode:
Diffstat (limited to 'lib/App/Acmeman/Source/Apache.pm')
-rw-r--r--lib/App/Acmeman/Source/Apache.pm284
1 files changed, 284 insertions, 0 deletions
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.