diff options
Diffstat (limited to 'lib/App/Acmeman/Source/Apache.pm')
-rw-r--r-- | lib/App/Acmeman/Source/Apache.pm | 284 |
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; |