]>
Raphaël G. Git Repositories - acme/blob - acme
553911e2b368a9ba5b7c77d911311638ca333c2e
   3 # This program is free software: you can redistribute it and/or modify 
   4 # it under the terms of the GNU General Public License as published by 
   5 # the Free Software Foundation, either version 3 of the License, or 
   6 # (at your option) any later version. 
   8 # This program is distributed in the hope that it will be useful, 
   9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  11 # GNU General Public License for more details. 
  13 # You should have received a copy of the GNU General Public License 
  14 # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  16 # Copyright (C) 2016 - 2020 Raphaël Gertz <acme@rapsys.eu> 
  24 #       # Allow use of print Dumper($this); 
  26 #       # Prepend current directory in include array 
  27 #       # XXX: this will load ./Acme.pm instead of an other version from system tree 
  31 # Add acl support to file tests 
  32 use filetest 
qw(access); 
  35 use Carp 
qw(carp confess); 
  36 use File
::Copy 
qw(copy); 
  37 use File
::stat qw(stat); 
  38 use File
::Slurp 
qw(read_file write_file); 
  39 use File
::Spec 
qw(splitpath curdir); 
  40 use JSON 
qw(decode_json); 
  42 use Net
::Domain
::TLD 
qw(tld_exists); 
  47 use POSIX 
qw(EXIT_SUCCESS EXIT_FAILURE); 
  60 #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys 
  61 tie
(%{$config}, 'Tie::IxHash'); 
  66 # Init config file name 
  67 my $configFilename = Acme
::CONFIG
; 
  73 @ARGV = map { if ($_ eq '-v' or $_ eq '--verbose') { $verbose = 1; (); } else { $_; } } @ARGV; 
  76 @ARGV = map { if ($_ eq '-d' or $_ eq '--debian') { $debian = 1; (); } else { $_; } } @ARGV; 
  79 unless(defined $ARGV[0] and $ARGV[0] =~ /^(cert|cron|conf)$/) { 
  80         print "Usage: $0 (cert|cron|conf) [-(v|-verbose)] [-(c|-config)[=/etc/acme/config]] [example.com] [...]\n"; 
  89 # Process config and args 
  90 for (my $i = 0; $i <= $#ARGV; $i++) { 
  92         if ($ARGV[$i] =~ /^(?:(\-c|\-\-config)(?:=(.+))?)$/) { 
  93                 # Extract -c=$2 or --config=$2 syntax 
  98                 # Extract -c $ARGV[$i+1] or --config $ARGV[$i+1] writable status 
  99                 } elsif (defined($ARGV[$i+1])) { 
 100                         $configFilename = $ARGV[$i+1]; 
 101                         splice(@ARGV, $i, 2); 
 103                 # Check if cert or cron action 
 104                 } elsif ($action eq 'cert' or $action eq 'cron') { 
 105                         print "Usage: $0 $action [-(c|-config)[=/etc/acme/config]] [example.com] [...]\n"; 
 109                 # Check if file don't exists 
 110                 if (defined($configFilename) and ! -f 
$configFilename) { 
 111                         # Extract config directory and filename 
 112                         my ($vol, $dir, $file) = File
::Spec-
>splitpath($configFilename); 
 116                                 # Set as current dir if empty 
 117                                 $dir = File
::Spec-
>curdir(); 
 120                         # Verify that directory exists 
 122                                 confess
('Config directory '.$dir.' must exists: '.$!); 
 125                         # Check that directory is writable 
 127                                 confess
('Config directory '.$dir.' must be writable: '.$!); 
 133 # Check if conf action 
 134 if ($action eq 'conf') { 
 136         my $js = JSON-
>new->utf8->pretty(1)->space_before(0)->filter_json_object( 
 138                         # Get source hash ref 
 141                         #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys 
 142                         my $r = tie
(my %r, 'Tie::IxHash'); 
 145                                 # Insert key if present 
 146                                 $r{$_} = $x->{$_} if (defined($x->{$_})); 
 147                         #XXX: Hash keys not present in this array will be dropped 
 148                         #XXX: Hash keys will be inserted in tied hash in this order 
 152                                 'thumbprint', 'term', 'pending', 'certificates', 
 154                                 'cert', 'key', 'account', 'mail', 'domain', 'domains', 'prod' 
 156                         # Return the ordered hash 
 161         # Check we have specified domains 
 162         unless (scalar(@ARGV) > 0) { 
 163                 print "Usage: $0 $action [-(v|-verbose)] [-(d|-debian)] [-(c|-config)[=/etc/acme/config]] example.com[=www.example.com[,ftp.example.com]] [...]\n"; 
 169                 #XXX: use eval to workaround a fatal in decode_json 
 172                         (-f 
$configFilename) and 
 174                         ($content = read_file
($configFilename)) and 
 176                         ($config = $js->decode($content)) 
 180                 carp
('Config file '.$configFilename.' not readable or invalid: '.$!) if ($verbose); 
 182                 # Create a default config 
 183                 #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys 
 184                 tie
(%{$config}, 'Tie::IxHash', 'thumbprint' => Acme
::THUMBPRINT
, 'term' => Acme
::TERM
, 'pending' => Acme
::PENDING
, 'certificates' => []); 
 186                 # Fix root infos when missing 
 187                 $config->{thumbprint
} = Acme
::THUMBPRINT 
unless(defined($config->{thumbprint
})); 
 188                 $config->{term
} = Acme
::TERM 
unless(defined($config->{term
})); 
 189                 $config->{pending
} = Acme
::PENDING 
unless(defined($config->{pending
})); 
 190                 $config->{certificates
} = [] unless(defined($config->{certificates
}) and ref($config->{certificates
}) eq 'ARRAY'); 
 193         # Iterate on each certificates entry 
 194         for (my $i = 0; $i <= $#{$config->{certificates
}}; $i++) { 
 196                 my $certificate = ${$config->{certificates
}}[$i]; 
 198                 # Drop the entry when missing domain key 
 199                 unless (defined($certificate->{domain
})) { 
 200                         splice(@{$config->{certificates
}}, $i, 1); 
 207                         #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys 
 208                         tie
(%{$replace}, 'Tie::IxHash', cert 
=> Acme
::RH_CERTS
.'/'.$certificate->{domain
}.Acme
::RH_SUFFIX
, key 
=> Acme
::RH_PRIVATE
.'/'.$certificate->{domain
}.Acme
::RH_SUFFIX
, account 
=> Acme
::ACCOUNT
, mail 
=> Acme
::MAIL
.'@'.$certificate->{domain
}, 'domain' => $certificate->{domain
}, 'domains' => [], prod 
=> 0); 
 211                                 $replace->{cert
} = Acme
::DEB_CERTS
.'/'.$certificate->{domain
}.Acme
::DEB_CERTS_SUFFIX
; 
 212                                 $replace->{key
} = Acme
::DEB_PRIVATE
.'/'.$certificate->{domain
}.Acme
::DEB_PRIVATE_SUFFIX
; 
 216                         $replace->{cert
} = $certificate->{cert
} if (defined($certificate->{cert
})); 
 219                         $replace->{key
} = $certificate->{key
} if (defined($certificate->{key
})); 
 222                         $replace->{account
} = $certificate->{account
} if (defined($certificate->{account
})); 
 225                         $replace->{mail
} = $certificate->{mail
} if (defined($certificate->{mail
})); 
 228                         $replace->{domains
} = $certificate->{domains
} if (defined($certificate->{domains
}) and ref($certificate->{domains
}) eq 'ARRAY'); 
 231                         $replace->{prod
} = $certificate->{prod
} if (defined($certificate->{prod
})); 
 233                         # Replace certificate 
 234                         ${$config->{certificates
}}[$i] = $replace; 
 238         # Check that domains are present in config 
 240                 # Extract domain and domains 
 241                 my ($domain, $domains) = split(/=/, $_); 
 244                 my @domains = defined($domains) ? map { $_ ? $_ : (); } split(/,/, $domains) : (); 
 251                         unless (($tld) = $_ =~ m/\.(\w+)$/) { 
 252                                 confess
('Extract '.$_.' tld failed'); 
 255                         # Check if tld exists 
 256                         unless(Net
::Domain
::TLD
::tld_exists
($tld)) { 
 257                                 confess
('Extracted '.$_.' tld '.$tld.' do not exists'); 
 261                         my $a = Net
::DNS
::Resolver-
>new->search($_, 'A', 'IN'); 
 264                         my $aaaa = Net
::DNS
::Resolver-
>new->search($_, 'AAAA', 'IN'); 
 266                         # Trigger error for unresolvable domain 
 268                                 # Check if either has a A or AAAA record 
 270                                         ($_->type eq 'A' or $_->type eq 'AAAA') ? 1 : (); 
 274                                         (defined $a and defined $a->answer) ? $a->answer : (), 
 275                                         (defined $aaaa and defined $aaaa->answer) ? $aaaa->answer : () 
 278                                 confess
('Resolve '.$_.' to an A or AAAA record failed'); 
 280                 } ($domain, @domains); 
 282                 # Insert domain when missing 
 283                 unless (scalar map { $_->{domain
} eq $domain ? 1 : (); } @{$config->{certificates
}}) { 
 285                         my $certificate = undef; 
 288                         #XXX: tie to Tie::IxHash to keep a stable ordering of hash keys 
 289                         tie
(%{$certificate}, 'Tie::IxHash', cert 
=> undef, key 
=> undef, account 
=> Acme
::ACCOUNT
, mail 
=> Acme
::MAIL
.'@'.$domain, 'domain' => $domain, 'domains' => [], prod 
=> 0); 
 293                                 $certificate->{cert
} = Acme
::DEB_CERTS
.'/'.$domain.Acme
::DEB_CERTS_SUFFIX
; 
 294                                 $certificate->{key
} = Acme
::DEB_PRIVATE
.'/'.$domain.Acme
::DEB_PRIVATE_SUFFIX
; 
 297                                 $certificate->{cert
} = Acme
::RH_CERTS
.'/'.$domain.Acme
::RH_SUFFIX
; 
 298                                 $certificate->{key
} = Acme
::RH_PRIVATE
.'/'.$domain.Acme
::RH_SUFFIX
; 
 306                                 # Check if already present 
 307                                 unless (scalar map { $_ eq $subdomain ? 1 : (); } @{$certificate->{domains
}}) { 
 308                                         # Add when not already present 
 309                                         ${$certificate->{domains
}}[scalar @{$certificate->{domains
}}] = $_; 
 314                         ${$config->{certificates
}}[scalar @{$config->{certificates
}}] = $certificate; 
 315                 # Update domains when present 
 317                         # Loop on all certificate 
 319                                 # Check that we are on the right domain 
 320                                 if ($_->{domain
} eq $domain) { 
 322                                         my $certificate = $_; 
 325                                         @{$certificate->{domains
}} = (); 
 332                                                 # Check if already present 
 333                                                 unless (scalar map { $_ eq $subdomain ? 1 : (); } @{$certificate->{domains
}}) { 
 334                                                         # Add when not already present 
 335                                                         ${$certificate->{domains
}}[scalar @{$certificate->{domains
}}] = $_; 
 339                         } @{$config->{certificates
}}; 
 343         # Extract config directory and filename 
 344         my ($vol, $dir, $file) = File
::Spec-
>splitpath($configFilename); 
 348                 # Set as current dir if empty 
 349                 $dir = File
::Spec-
>curdir(); 
 352         # Backup old config if possible 
 353         if (-w 
$dir and -f 
$configFilename) { 
 354                 my ($dt, $suffix) = undef; 
 356                 # Extract datetime suffix 
 357                 $suffix = ($dt = DateTime-
>from_epoch(epoch 
=> stat($configFilename)->mtime))->ymd('').$dt->hms(''); 
 360                 unless(copy
($configFilename, $configFilename.'.'.$suffix)) { 
 361                         carp
('Copy '.$configFilename.' to '.$configFilename.'.'.$suffix.' failed: '.$!); 
 363         # Check that directory is writable 
 364         } elsif (! -w 
$dir and -f 
$configFilename) { 
 365                 confess
('Config directory '.$dir.' must be writable: '.$!); 
 368         # Encode config in json 
 369         #XXX: emulate a tab indent file by replacing 3 space indent with tab 
 370         ($content = $js->encode($config)) =~ s/(\G|^)\s{3}/\t/gm; 
 373         write_file
($configFilename, $content); 
 377 # Check if cert or cron action 
 378 } elsif ($action eq 'cert' or $action eq 'cron') { 
 381                 #XXX: use eval to workaround a fatal in decode_json 
 384                         (-f 
$configFilename) and 
 386                         ($content = read_file
($configFilename)) and 
 388                         ($config = decode_json
($content)) and 
 389                         # Check certificates presence 
 390                         defined($config->{certificates
}) and 
 391                         # Check certificates type 
 392                         ref($config->{certificates
}) eq 'ARRAY' and 
 393                         # Check thumbprint presence 
 394                         defined($config->{thumbprint
}) and 
 395                         # Check term presence 
 396                         defined($config->{term
}) and 
 397                         # Check pending presence 
 398                         defined($config->{pending
}) and 
 399                         # Check certificates array 
 402                                         defined($_->{cert
}) and 
 403                                         defined($_->{key
}) and 
 404                                         defined($_->{account
}) and 
 405                                         defined($_->{mail
}) and 
 406                                         defined($_->{domain
}) and 
 407                                         defined($_->{domains
}) and ref($_->{domains
}) eq 'ARRAY' and 
 414                         } @{$config->{certificates
}} 
 417                 confess
('Config file '.$configFilename.' not readable or invalid: '.$!); 
 421         #TODO: implement the new action 
 422         confess
('Unknown '.$action.' action'); 
 425 # Deal with specified domains 
 426 if (scalar(@ARGV) > 0) { 
 427         # Check that domains are present in config 
 428         foreach my $domain (@ARGV) { 
 430                 foreach my $certificate (@{$config->{certificates
}}) { 
 431                         if ($certificate->{domain
} eq $domain) { 
 432                                 push(@domains, $certificate); 
 437                         print 'Domain '.$domain.' not found in config file '.$configFilename."\n"; 
 443         # Populate domains array with available ones 
 444         foreach my $certificate (@{$config->{certificates
}}) { 
 445                 push(@domains, $certificate); 
 450 if (scalar(@domains) < 1) { 
 451         print "Usage: $0 conf [-(v|-verbose)] [-(d|-debian)] [-(c|-config)[=/etc/acme/config]] example.com[=www.example.com[,ftp.example.com]] [...]\n"; 
 455 # Deal with each domain 
 456 foreach my $domain (@domains) { 
 457         # Skip certificate, in cron action, issued within the last 60 days 
 458         if ($action eq 'cron' and -f 
$domain->{cert
} and stat($domain->{cert
})->mtime >= (time() - 60*24*3600)) { 
 459                 carp
('Domain '.$domain->{domain
}.' certificate '.$domain->{cert
}.' skipped') if ($verbose); 
 463         my $acme = Acme-
>new($verbose, $domain, {thumbprint 
=> $config->{thumbprint
}, pending 
=> $config->{pending
}, term 
=> $config->{term
}}); 
 465         # Prepare environement 
 468         # Generate required keys