#!/usr/bin/perl package SPIN::LE; use strict; use warnings; use autodie qw(:all); use Date::Parse; use LWP::UserAgent; use Getopt::Long; { our @restart_services_cmd = qw(sudo service httpd graceful); our @openssl_bin = qw(/usr/bin/openssl); our @acme_tiny_bin = qw(/usr/bin/python acme-tiny/acme_tiny.py); our $account_key_path = 'data/account.key'; our $challenges_dir = 'data/challenges/'; our $days_left_limit = 14; our $csr_dir = 'data'; our $config_file; GetOptions("config=s" => \$config_file); require $config_file if defined $config_file; sub get_intermediate_cert_url { my $crt_fn = shift; open my $openssl, '-|', @openssl_bin, 'x509', '-in', $crt_fn, '-text', '-noout'; while(my $line = <$openssl>) { return $1 if $line =~ m|^\s*CA Issuers - URI:(.*?)\s*$|; } die("Cert URL not found"); } sub get_intermediate_cert { my $crt_fn = shift; my $pem_fn = shift; my $der_fn = "$pem_fn.tmp"; my $url = get_intermediate_cert_url($crt_fn); my $ua = LWP::UserAgent->new; my $response = $ua->mirror($url, $der_fn); die("Failed to download $url\n") if !($response->is_success); system(@openssl_bin, 'x509', '-inform', 'der', '-in', $der_fn, '-out', $pem_fn); unlink($der_fn); } sub get_cert { my ($csr_fn, $crt_fn, $int_crt_fn) = @_; open my $acme_tiny, '-|', @acme_tiny_bin, '--account-key', $account_key_path, '--csr', $csr_fn, '--acme-dir', $challenges_dir; my $cert_start = 0; my $cert_end = 0; my $lines = ''; while (my $line = <$acme_tiny>) { print $line; $cert_start = 1 if $line =~ /^-----BEGIN CERTIFICATE-----$/; $cert_end = 1 if $line =~ /^-----END CERTIFICATE-----$/; $lines .= $line; } if ($cert_start && $cert_end) { my $tmp_fn = "$crt_fn.tmp"; my $int_tmp_fn = "$int_crt_fn.tmp"; open my $crt, '>', $tmp_fn; print $crt $lines or die("Write failed: $!\n"); close $crt; get_intermediate_cert($tmp_fn, $int_tmp_fn); rename $tmp_fn,$crt_fn; rename $int_tmp_fn,$int_crt_fn; } else { die("Output doesn't look like a certificate\n"); } } sub renew { my %fmap = @_; my $certs_updated = 0; for my $csr_fn (keys %fmap) { my $crt_fn = $fmap{$csr_fn}->[0]; my $int_crt_fn = $fmap{$csr_fn}->[1]; if (!-e $crt_fn) { print "Getting new certificate $crt_fn\n"; get_cert($csr_fn, $crt_fn, $int_crt_fn); next; } open my $openssl, '-|', @openssl_bin, 'x509', '-enddate', '-noout', '-in', $crt_fn; my $ret = <$openssl>; if ($ret =~ /^notAfter=(.*)$/) { my $date_str = $1; my $time = str2time($date_str); if (defined $time) { if ($time - time < $days_left_limit*24*60*60) { my $days = (($time - time)/60/60/24)|0; print "Certificate $crt_fn "; if ($days >= 1) { print "will expire in $days days" } elsif ($days == 0) { print "will expire today" } elsif ($days == -1) { print "expired yesterday" } else { printf "expired %d days ago", -$days }; print "\nRenewing...\n"; get_cert($csr_fn, $crt_fn, $int_crt_fn); $certs_updated++; } } else { warn "Failed parsing expiration date for $crt_fn: openssl returned $ret\n"; } } else { warn "Failed getting expiration date for $crt_fn: openssl returned $ret\n"; } } return $certs_updated; } sub run { opendir(my $dh, $csr_dir); my %fmap = map { my $base = $_; $base =~ s/\.csr$//; $_ => [ "$base.crt", "$base-int.crt" ] } map { "$csr_dir/$_" } grep { /\.csr$/ } readdir $dh; system(@restart_services_cmd) if renew(%fmap); } } __PACKAGE__->run() unless caller();