278 lines
8.2 KiB
Perl
Executable File
278 lines
8.2 KiB
Perl
Executable File
#!/usr/bin/perl
|
|
# Request a new SSL cert using Let's Encrypt
|
|
|
|
use strict;
|
|
use warnings;
|
|
no warnings 'redefine';
|
|
no warnings 'uninitialized';
|
|
|
|
require "./webmin-lib.pl";
|
|
our %text;
|
|
our %miniserv;
|
|
our %in;
|
|
our $config_directory;
|
|
our %config;
|
|
our $module_name;
|
|
our $letsencrypt_cmd;
|
|
&error_setup($text{'letsencrypt_err'});
|
|
|
|
my $letsencrypt_ip_cert_renewal_max_days = 5;
|
|
|
|
# Re-check if let's encrypt is available
|
|
my $err = &check_letsencrypt();
|
|
&error($err) if ($err);
|
|
|
|
# Validate inputs
|
|
&ReadParse();
|
|
my @doms = split(/\s+/, $in{'dom'});
|
|
foreach my $dom (@doms) {
|
|
$dom =~ /^(\*\.)?[a-z0-9\-\.\_]+$/i ||
|
|
&letsencrypt_is_ip_cert_identifier($dom) ||
|
|
&error($text{'letsencrypt_edom'});
|
|
}
|
|
my $has_ip_doms = &letsencrypt_doms_have_ips(\@doms);
|
|
$in{'directory_url'} = &trim($in{'directory_url'});
|
|
$in{'eab_kid'} = &trim($in{'eab_kid'});
|
|
$in{'eab_hmac'} = &trim($in{'eab_hmac'});
|
|
if ($in{'directory_url'}) {
|
|
my ($host, $port, $page, $ssl) = &parse_http_url($in{'directory_url'});
|
|
$host && $ssl < 2 || &error($text{'letsencrypt_eacmedir'});
|
|
}
|
|
if ($in{'eab_kid'} || $in{'eab_hmac'}) {
|
|
$in{'directory_url'} || &error($text{'letsencrypt_eeabdir'});
|
|
$in{'eab_kid'} && $in{'eab_hmac'} ||
|
|
&error($text{'letsencrypt_eeabpair'});
|
|
$letsencrypt_cmd || &error($text{'letsencrypt_eeabnative'});
|
|
}
|
|
if (!$in{'renew_def'}) {
|
|
$in{'renew'} =~ /^[1-9][0-9]*$/ ||
|
|
&error($text{'letsencrypt_erenew'});
|
|
$in{'renew_unit'} ||= $has_ip_doms ? "days" : "months";
|
|
$in{'renew_unit'} =~ /^(days|months)$/ ||
|
|
&error($text{'letsencrypt_erenewunit'});
|
|
my $ip_max_days = $letsencrypt_ip_cert_renewal_max_days;
|
|
if ($has_ip_doms && ($in{'renew_unit'} ne "days" ||
|
|
$in{'renew'} > $ip_max_days)) {
|
|
&error(&text('letsencrypt_erenewipauto',
|
|
$ip_max_days));
|
|
}
|
|
}
|
|
$in{'size_def'} || $in{'size'} =~ /^\d+$/ ||
|
|
&error($text{'newkey_esize'});
|
|
my $size = $in{'size_def'} ? undef : $in{'size'};
|
|
my $webroot;
|
|
my $mode = "web";
|
|
if ($in{'webroot_mode'} == 3) {
|
|
# Validation via DNS
|
|
$mode = "dns";
|
|
}
|
|
elsif ($in{'webroot_mode'} == 4) {
|
|
# Validation via Certbot webserver
|
|
$mode = "certbot";
|
|
}
|
|
elsif ($in{'webroot_mode'} == 2) {
|
|
# Some directory
|
|
$in{'webroot'} =~ /^\/\S+/ && -d $in{'webroot'} ||
|
|
&error($text{'letsencrypt_ewebroot'});
|
|
$webroot = $in{'webroot'};
|
|
}
|
|
else {
|
|
# Apache domain
|
|
&foreign_require("apache");
|
|
my $conf = &apache::get_config();
|
|
foreach my $virt (&apache::find_directive_struct(
|
|
"VirtualHost", $conf)) {
|
|
my $sn = &apache::find_directive(
|
|
"ServerName", $virt->{'members'});
|
|
my @sa = &apache::find_directive(
|
|
"ServerAlias", $virt->{'members'});
|
|
my $match = 0;
|
|
if ($in{'webroot_mode'} == 0 &&
|
|
&indexof($doms[0], $sn, @sa) >= 0) {
|
|
# Based on domain name
|
|
$match = 1;
|
|
}
|
|
elsif ($in{'webroot_mode'} == 1 && $sn eq $in{'vhost'}) {
|
|
# Specifically selected domain
|
|
$match = 1;
|
|
}
|
|
my @ports;
|
|
foreach my $w (@{$virt->{'words'}}) {
|
|
if ($w =~ /:(\d+)$/) {
|
|
push(@ports, $1);
|
|
}
|
|
else {
|
|
push(@ports, 80);
|
|
}
|
|
}
|
|
if ($match && &indexof(80, @ports) >= 0) {
|
|
# Get document root
|
|
$webroot = &apache::find_directive(
|
|
"DocumentRoot", $virt->{'members'}, 1);
|
|
$webroot || &error(&text('letsencrypt_edroot', $sn));
|
|
last;
|
|
}
|
|
}
|
|
$webroot || &error(&text('letsencrypt_evhost', $doms[0]));
|
|
}
|
|
|
|
if ($in{'save'}) {
|
|
# Just update renewal
|
|
&save_renewal_only(\@doms, $webroot, $mode, $size,
|
|
$in{'subset'}, $in{'use'},
|
|
$in{'directory_url'},
|
|
$in{'eab_kid'}, $in{'eab_hmac'});
|
|
&redirect("edit_ssl.cgi?mode=lets");
|
|
}
|
|
else {
|
|
# Request the cert
|
|
&ui_print_unbuffered_header(undef, $text{'letsencrypt_title'}, "");
|
|
|
|
print &text($mode eq 'dns' ? 'letsencrypt_doingdns' :
|
|
$mode eq 'certbot' ? 'letsencrypt_doingcertbot' :
|
|
'letsencrypt_doing',
|
|
"<tt>".&html_escape(join(", ", @doms))."</tt>",
|
|
"<tt>".&html_escape($webroot)."</tt>"),"<br>\n";
|
|
my ($ok, $cert, $key, $chain) = &request_letsencrypt_cert(
|
|
\@doms, $webroot, undef, $size, $mode, $in{'staging'},
|
|
undef, undef, undef, $in{'directory_url'},
|
|
$in{'eab_kid'}, $in{'eab_hmac'}, $in{'subset'});
|
|
if (!$ok) {
|
|
print &text('letsencrypt_failed', $cert),"\n";
|
|
}
|
|
else {
|
|
# Worked, now copy to Webmin
|
|
my @grid = ( $text{'letsencrypt_cert'}, $cert,
|
|
$text{'letsencrypt_key'}, $key );
|
|
push(@grid, $text{'letsencrypt_chain'}, $chain) if ($chain);
|
|
my $details = &html_escape($text{'letsencrypt_show'})."<p>\n".
|
|
&ui_grid_table(\@grid, 2);
|
|
print &ui_details({
|
|
'html' => 1,
|
|
'title' => &ui_tag('span',
|
|
&html_escape($text{'letsencrypt_done'}),
|
|
{ 'data-second-print' => undef }),
|
|
'content' => $details,
|
|
'class' => 'inline inlined',
|
|
});
|
|
print "<div data-x-br=\"\"></div>\n";
|
|
|
|
# Save the renewal schedule
|
|
&save_renewal_only(\@doms, $webroot, $mode,
|
|
$size, $in{'subset'}, $in{'use'},
|
|
$in{'directory_url'},
|
|
$in{'eab_kid'}, $in{'eab_hmac'}, 1);
|
|
|
|
# Copy cert, key and chain to Webmin
|
|
if ($in{'use'}) {
|
|
print $text{'letsencrypt_webmin'},"<br>\n";
|
|
&lock_file($ENV{'MINISERV_CONFIG'});
|
|
&get_miniserv_config(\%miniserv);
|
|
|
|
$miniserv{'keyfile'} = $config_directory.
|
|
"/letsencrypt-key.pem";
|
|
&lock_file($miniserv{'keyfile'});
|
|
©_source_dest($key, $miniserv{'keyfile'}, 1);
|
|
&unlock_file($miniserv{'keyfile'});
|
|
|
|
$miniserv{'certfile'} = $config_directory.
|
|
"/letsencrypt-cert.pem";
|
|
&lock_file($miniserv{'certfile'});
|
|
©_source_dest($cert, $miniserv{'certfile'}, 1);
|
|
&unlock_file($miniserv{'certfile'});
|
|
|
|
if ($chain) {
|
|
$miniserv{'extracas'} = $config_directory.
|
|
"/letsencrypt-ca.pem";
|
|
&lock_file($miniserv{'extracas'});
|
|
©_source_dest($chain, $miniserv{'extracas'}, 1);
|
|
&unlock_file($miniserv{'extracas'});
|
|
}
|
|
else {
|
|
delete($miniserv{'extracas'});
|
|
}
|
|
&put_miniserv_config(\%miniserv);
|
|
&unlock_file($ENV{'MINISERV_CONFIG'});
|
|
|
|
&webmin_log("letsencrypt");
|
|
&restart_miniserv(1);
|
|
print &ui_tag('span',
|
|
&html_escape($text{'letsencrypt_wdone'}),
|
|
{ 'data-second-print' => undef });
|
|
print "<br><div data-x-br=\"\"></div>\n";
|
|
}
|
|
}
|
|
|
|
&ui_print_footer("", $text{'index_return'});
|
|
}
|
|
|
|
# save_renewal_only(&doms, webroot, mode, size, subset-mode, used-by-webmin,
|
|
# directory-url, eab-kid, eab-hmac, [reset-renewal-time])
|
|
# Save for future renewals
|
|
sub save_renewal_only
|
|
{
|
|
my ($doms, $webroot, $mode, $size, $subset, $usewebmin, $directory_url,
|
|
$eab_kid, $eab_hmac, $reset_renewal_time) = @_;
|
|
$config{'letsencrypt_doms'} = join(" ", @$doms);
|
|
$config{'letsencrypt_webroot'} = $webroot;
|
|
$config{'letsencrypt_mode'} = $mode;
|
|
$config{'letsencrypt_size'} = $size;
|
|
$config{'letsencrypt_subset'} = $subset;
|
|
$config{'letsencrypt_nouse'} = $usewebmin ? 0 : 1;
|
|
my $renew_has_ip_doms = &letsencrypt_doms_have_ips($doms);
|
|
my $renew_unit = $in{'renew_unit'} =~ /^(days|months)$/ ?
|
|
$in{'renew_unit'} :
|
|
($renew_has_ip_doms ? "days" : "months");
|
|
$config{'letsencrypt_renew_unit'} = $renew_unit;
|
|
if ($directory_url) {
|
|
$config{'letsencrypt_directory_url'} = $directory_url;
|
|
}
|
|
else {
|
|
delete($config{'letsencrypt_directory_url'});
|
|
}
|
|
if ($directory_url && $eab_kid) {
|
|
$config{'letsencrypt_eab_kid'} = $eab_kid;
|
|
}
|
|
else {
|
|
delete($config{'letsencrypt_eab_kid'});
|
|
}
|
|
if ($directory_url && $eab_hmac) {
|
|
$config{'letsencrypt_eab_hmac'} = $eab_hmac;
|
|
}
|
|
else {
|
|
delete($config{'letsencrypt_eab_hmac'});
|
|
}
|
|
&save_module_config();
|
|
if (&foreign_check("webmincron")) {
|
|
my $job = &find_letsencrypt_cron_job();
|
|
if ($in{'renew_def'}) {
|
|
&webmincron::delete_webmin_cron($job) if ($job);
|
|
}
|
|
else {
|
|
# A manual cert request does not update Webmin cron's last-run
|
|
# state, so re-create the job to start the elapsed renewal
|
|
# interval from now instead of immediately running an overdue
|
|
# job with the old ID
|
|
if ($job && $reset_renewal_time) {
|
|
&webmincron::delete_webmin_cron($job);
|
|
$job = undef;
|
|
}
|
|
$job ||= { 'module' => $module_name,
|
|
'func' => 'renew_letsencrypt_cert' };
|
|
# Scheduling is driven by 'interval' (elapsed seconds).
|
|
# 'months' is retained only for month-based schedules so
|
|
# edit_ssl.cgi can show the renewal interval.
|
|
$job->{'mins'} = '';
|
|
$job->{'hours'} = '';
|
|
$job->{'days'} = '';
|
|
$job->{'months'} = $renew_unit eq "months" ?
|
|
'*/'.$in{'renew'} : '';
|
|
$job->{'weekdays'} = '';
|
|
$job->{'interval'} = $renew_unit eq "days" ?
|
|
$in{'renew'}*24*60*60 :
|
|
$in{'renew'}*30*24*60*60;
|
|
&webmincron::create_webmin_cron($job);
|
|
}
|
|
}
|
|
}
|