App-Acmeman
view release on metacpan or search on metacpan
lib/App/Acmeman.pm view on Meta::CPAN
package App::Acmeman;
use strict;
use warnings;
use Net::ACME2::LetsEncrypt;
use Crypt::Format;
use Crypt::OpenSSL::PKCS10 qw(:const);
use Crypt::OpenSSL::RSA;
use Crypt::OpenSSL::X509;
use File::Basename;
use File::Path qw(make_path);
use File::Spec;
use DateTime::Format::Strptime;
use LWP::UserAgent;
use LWP::Protocol::https;
use Socket qw(inet_ntoa);
use Sys::Hostname;
use Pod::Usage;
use Pod::Man;
use Getopt::Long qw(:config gnu_getopt no_ignore_case);
use POSIX qw(strftime time floor);
use App::Acmeman::Config;
use App::Acmeman::Domain qw(:files);
use Data::Dumper;
use Text::ParseWords;
use App::Acmeman::Log qw(:all :sysexits);
use feature 'state';
our $VERSION = '3.10';
my $progdescr = "manages ACME certificates";
our $acme_dir = '/etc/ssl/acme';
our $letsencrypt_root_cert_basename = 'lets-encrypt-root.pem';
our $letsencrypt_root_cert_url =
'https://letsencrypt.org/certs/lets-encrypt-r3-cross-signed.pem';
sub new {
my $class = shift;
my $self = bless {
_progname => basename($0),
_acme_host => 'production',
_command => 'renew',
_option => {
config_file => '/etc/acmeman.conf'
},
_domains => []
}, $class;
GetOptions(
'h' => sub {
pod2usage(-message => "$self->{_progname}: $progdescr",
-exitstatus => EX_OK);
},
'help' => sub {
pod2usage(-exitstatus => EX_OK, -verbose => 2);
},
'usage' => sub {
pod2usage(-exitstatus => EX_OK, -verbose => 0);
},
'debug|d+' => \$self->{_option}{debug},
'dry-run|n' => \$self->{_option}{dry_run},
'stage|s' => sub { $self->{_acme_host} = 'staging' },
'force|F' => \$self->{_option}{force},
'time-delta|D=n' => \$self->{_option}{time_delta},
'setup|S' => sub { $self->{_command} = 'setup' },
'alt-names|a' => \$self->{_option}{check_alt_names},
'config-file|f=s' => \$self->{_option}{config_file},
'version' => sub {
print "$0 version $VERSION\n";
exit(EX_OK)
}
) or exit(EX_USAGE);
++$self->{_option}{debug} if $self->dry_run_option;
lib/App/Acmeman.pm view on Meta::CPAN
$vhost->certificate_file;
local $ENV{ACMEMAN_DOMAIN_NAME} = $vhost;
local $ENV{ACMEMAN_ALT_NAMES} = join(' ', $vhost->alt);
foreach my $cmd (@$postrenew) {
$self->runcmd($cmd, $vhost);
}
} else {
push @renewed, $vhost;
}
}
}
}
if (@renewed) {
local $ENV{ACMEMAN_CERTIFICATE_COUNT} = @renewed;
local $ENV{ACMEMAN_CERTIFICATE_FILE} =
join(' ', map { $_->certificate_file } @renewed);
local $ENV{ACMEMAN_DOMAIN_NAME} =
join(' ', map { "$_" } @renewed);
local $ENV{ACMEMAN_ALT_NAMES} =
join(' ', map { ($_->alt) } @renewed);
if ($self->cf->is_set(qw(core postrenew))) {
foreach my $cmd (grep { defined($_) }
$self->cf->get(qw(core postrenew))) {
$self->runcmd($cmd);
}
} else {
error("certificates changed, but no postrenew command is defined (core.postrenew)");
}
}
}
sub domain_cert_expires {
my ($self, $domain) = @_;
my $crt = $domain->certificate_file;
if (-f $crt) {
my $x509 = Crypt::OpenSSL::X509->new_from_file($crt);
my $exts = $x509->extensions_by_name();
if (exists($exts->{subjectAltName})) {
my $msg = $self->cf->get(qw(core check-alt-names))
? 'will renew' : 'use -a to trigger renewal';
my @names = map { s/^DNS://; $_ }
split /,\s*/, $exts->{subjectAltName}->to_string();
my @missing;
foreach my $vh (sort { length($b) <=> length($a) } $domain->names) {
unless (grep { $_ eq $vh } @names) {
push @missing, $vh;
}
}
if (@missing) {
debug(1, "$crt: the following SANs are missing: "
. join(', ', @missing)
. "; $msg");
return 1 if $self->cf->get(qw(core check-alt-names));
}
}
my $expiry = $x509->notAfter();
my $strp = DateTime::Format::Strptime->new(
pattern => '%b %d %H:%M:%S %Y %Z',
time_zone => 'GMT'
);
my $ts = $strp->parse_datetime($expiry)->epoch;
my $now = time();
if ($now < $ts) {
my $hours = floor(($ts - $now) / 3600);
my $in;
if ($hours > 24) {
my $days = floor($hours / 24);
$in = "in $days days";
} elsif ($hours == 24) {
$in = "in one day";
} else {
$in = "today";
}
debug(2, "$crt expires on $expiry, $in");
if ($now + $self->cf->get(qw(core time-delta)) <= $ts) {
return 0;
} else {
debug(2, "will renew $crt (expires on $expiry, $in)");
}
} else {
debug(2, "will renew $crt");
}
}
return 1;
}
sub debug_to_loglevel {
my $self = shift;
my @lev = ('err', 'info', 'debug');
my $v = $self->cf->core->verbose;
return $lev[$v > $#lev ? $#lev : $v];
}
my @challenge_files;
END {
if (@challenge_files) {
debug(3, "removing challenge files");
my $n = unlink @challenge_files;
unless ($n == @challenge_files) {
error("some challenge files were not removed",
prefix => 'warning');
}
}
}
sub save_challenge {
my ($self,$challenge) = @_;
my $file = File::Spec->catfile($self->cf->get(qw(core rootdir)), $challenge->get_path);
if (open(my $fh, '>', $file)) {
print $fh $self->acme->make_key_authorization($challenge);
close $fh;
debug(3, "wrote challenge file $file");
push @challenge_files, $file;
} else {
error("can't open $file for writing: $!");
die;
( run in 0.753 second using v1.01-cache-2.11-cpan-39bf76dae61 )