#!/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', "".&html_escape(join(", ", @doms))."", "".&html_escape($webroot).""),"
\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'})."

\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 "

\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'},"
\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 "
\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); } } }