
sub init_ssl
{
$feature_depends{'ssl'} = [ 'web', 'dir' ];
$default_web_sslport = $config{'web_sslport'} || 443;
}

# feature_depends_ssl()
# Return undef if SSL feature prerequisites are met
sub feature_depends_ssl
{
my $werr = &feature_depends_web();
return $werr if ($werr);
$config{'web'} || return &text('check_edepssl', "edit_newfeatures.cgi");
return undef;
}

# check_warnings_ssl(&dom, &old-domain)
# An SSL website should have either a private IP, or private port, UNLESS
# the clashing domain's cert can be used for this domain.
sub check_warnings_ssl
{
my ($d, $oldd) = @_;
&require_apache();
my $tmpl = &get_template($d->{'template'});
my $defport = $tmpl->{'web_sslport'} || 443;
my $port = $d->{'web_sslport'} || $defport;

# Check if Apache supports SNI, which makes clashing certs not so bad
my $sni = &has_sni_support($d);

if ($port != $defport) {
	# Has a private port
	return undef;
	}
elsif ($sni) {
	# Web server and clients can handle multiple SSL certs on
	# the same IP address
	return undef;
	}
else {
	# Neither .. but we can still do SSL, if there are no other domains
	# with SSL on the same IPv4 address
	if ($d->{'ip'}) {
		my ($sslclash) = grep { $_->{'ip'} eq $d->{'ip'} &&
					$_->{'ssl'} &&
					$_->{'id'} ne $d->{'id'} }
				      &list_domains();
		if (!$d->{'virt'} && $sslclash &&
		    (!$oldd || !$oldd->{'ssl'})) {
			# Clash .. but is the cert OK?
			if (!&check_domain_certificate($d->{'dom'},
						       $sslclash)) {
				my @certdoms =
					&list_domain_certificate($sslclash);
				return &text('setup_edepssl5', $d->{'ip'},
				    join(", ", map { "<tt>$_</tt>" } @certdoms),
				    $sslclash->{'dom'});
				}
			else {
				return undef;
				}
			}
		}

	# Check for <virtualhost> on the IP, if we are turning on SSL
	if ($d->{'ip'} && (!$oldd || !$oldd->{'ssl'})) {
		&require_apache();
		my $conf = &apache::get_config();
		foreach my $v (&apache::find_directive_struct("VirtualHost",
							      $conf)) {
			foreach my $w (@{$v->{'words'}}) {
				my ($vip, $vport) = split(/:/, $w);
				if ($vip eq $d->{'ip'} && $vport == $port) {
					return &text('setup_edepssl4',
						     $d->{'ip'}, $port);
					}
				}
			}
		}

	# Perform the same check on IPv6
	if ($d->{'ip6'}) {
		my ($sslclash6) = grep { $_->{'ip6'} &&
					 $_->{'ip6'} eq $d->{'ip6'} &&
					 $_->{'ssl'} &&
					 $_->{'id'} ne $d->{'id'} }
				       &list_domains();
		if (!$d->{'virt6'} && $sslclash6 &&
		    (!$oldd || !$oldd->{'ssl'})) {
			# Clash .. but is the cert OK?
			if (!&check_domain_certificate($d->{'dom'},
						       $sslclash6)) {
				my @certdoms =
					&list_domain_certificate($sslclash6);
				return &text('setup_edepssl5', $d->{'ip6'},
				    join(", ", map { "<tt>$_</tt>" } @certdoms),
				    $sslclash6->{'dom'});
				}
			else {
				return undef;
				}
			}
		}

	# Check for <virtualhost> on the IPv6 address, if we are turning on SSL
	if ($d->{'ip6'} && (!$oldd || !$oldd->{'ssl'})) {
		&require_apache();
		my $conf = &apache::get_config();
		foreach my $v (&apache::find_directive_struct("VirtualHost",
							      $conf)) {
			foreach my $w (@{$v->{'words'}}) {
				$w =~ /^\[([^\]]+)\]:(\d+)$/ || next;
				my ($vip, $vport) = ($1, $2);
				if ($vip eq $d->{'ip6'} && $vport == $port) {
					return &text('setup_edepssl4',
						     $d->{'ip6'}, $port);
					}
				}
			}
		}

	return undef;
	}
}

# setup_ssl(&domain)
# Creates a website with SSL enabled, and a private key and cert it to use.
sub setup_ssl
{
my ($d) = @_;
my $tmpl = &get_template($d->{'template'});
my $web_sslport = $d->{'web_sslport'} || $tmpl->{'web_sslport'} || 443;
&require_apache();
&obtain_lock_web($d);
my $conf = &apache::get_config();
$d->{'letsencrypt_renew'} = 1;		# Default let's encrypt renewal

# Find out if this domain will share a cert with another
&find_matching_certificate($d);

# Create a self-signed cert and key, if needed
my $generated = &generate_default_certificate($d);
if (!$generated && !-r $d->{'ssl_cert'}) {
	# SSL cert failed, so don't proceed
	return 0;
	}
&refresh_ssl_cert_expiry($d);
my $chained = $d->{'ssl_chain'};
&sync_combined_ssl_cert($d);

# Add NameVirtualHost if needed, and if there is more than one SSL site on
# this IP address
my ($nvstar, $nvstar6);
if ($d->{'ip'}) {
	$nvstar = &add_name_virtual($d, $conf, $web_sslport, 1, $d->{'ip'});
	}
if ($d->{'ip6'}) {                                
        $nvstar6 = &add_name_virtual($d, $conf, $web_sslport, 1, $d->{'ip6'});
        }       

# Add a Listen directive if needed
&add_listen($d, $conf, $web_sslport);

# Find directives in the non-SSL virtualhost, for copying
&$first_print($text{'setup_ssl'});
my $already = &get_apache_virtual($d->{'dom'}, $web_sslport);
if ($already) {
	&$second_print(&text('setup_webclash', $d->{'dom'}, $web_sslport));
	return 0;
	}
my ($virt, $vconf) = &get_apache_virtual($d->{'dom'},
					    $d->{'web_port'});
if (!$virt) {
	&$second_print($text{'setup_esslcopy'});
	return 0;
	}

# Double-check cert and key
my $certdata = &read_file_contents($d->{'ssl_cert'});
my $keydata = &read_file_contents($d->{'ssl_key'});
my $err = &validate_cert_format($certdata, 'cert');
if ($err) {
	&$second_print(&text('setup_esslcert', $err));
	return 0;
	}
my $err = &validate_cert_format($keydata, 'key');
if ($err) {
	&$second_print(&text('setup_esslkey', $err));
	return 0;
	}
if ($d->{'ssl_chain'}) {
	my $cadata = &read_file_contents($d->{'ssl_chain'});
	my $err = &validate_cert_format($cadata, 'ca');
	if ($err) {
		&$second_print(&text('setup_esslca', $err));
		return 0;
		}
	}
my $err = &check_cert_key_match($certdata, $keydata);
if ($err) {
	&$second_print(&text('setup_esslmatch', $err));
	return 0;
	}

# Add the actual <VirtualHost>
my $f = $virt->{'file'};
my @mems = &clone_apache_config($virt->{'members'});
my @ssldirs = &apache_ssl_directives($d, $tmpl);
push(@mems, &apache_lines_to_config(\@ssldirs));
my $vips = &get_apache_vhost_ips($d, $nvstar, $nvstar6, $web_sslport);
my $newvirt = { 'name' => 'VirtualHost',
		'value' => $vips,
		'file' => $f,
		'type' => 1,
		'members' => \@mems };
&apache::save_directive_struct(undef, $newvirt, $conf, $conf);
&flush_file_lines($f);

# Copy chained CA cert in from domain with same IP, if any
$d->{'web_sslport'} = $web_sslport;
if ($chained) {
	&save_website_ssl_file($d, 'ca', $chained);
	}
$d->{'web_urlsslport'} = $tmpl->{'web_urlsslport'};

# Add cert in Webmin, Dovecot, etc..
&enable_domain_service_ssl_certs($d);

# Update DANE DNS records
&sync_domain_tlsa_records($d);

# Redirect HTTP to HTTPS
if ($d->{'auto_redirect'} // $tmpl->{'web_sslredirect'}) {
	&create_redirect($d, &get_redirect_to_ssl($d));
	}

&release_lock_web($d);
&$second_print($text{'setup_done'});
if ($d->{'virt'}) {
	&register_post_action(\&restart_apache, &ssl_needs_apache_restart());
	}
else {
	&register_post_action(\&restart_apache);
	}

# Try to request a Let's Encrypt cert when enabling SSL post-creation for
# the first time
if (!$d->{'creating'} && $generated && $d->{'auto_letsencrypt'} &&
    !$d->{'disabled'}) {
	&create_initial_letsencrypt_cert($d, 1);
	}

return 1;
}

# setup_alias_ssl(&alias-target, &domain)
# Called when a domain with SSL gets an alias
sub setup_alias_ssl
{
my ($aliasd, $d) = @_;
my @certs = &get_all_domain_service_ssl_certs($aliasd);
&update_all_domain_service_ssl_certs($aliasd, \@certs);
}

# delete_alias_ssl(&alias-target, &domain)
# Called when a domain with SSL loses an alias
sub delete_alias_ssl
{
my ($aliasd, $d) = @_;
my @certs = &get_all_domain_service_ssl_certs($aliasd);
&update_all_domain_service_ssl_certs($aliasd, \@certs);
}

# modify_ssl(&domain, &olddomain)
sub modify_ssl
{
my ($d, $oldd) = @_;
my $rv = 0;
&require_apache();
&obtain_lock_web($d);

# Get objects for SSL and non-SSL virtual hosts
my ($virt, $vconf, $conf) = &get_apache_virtual($oldd->{'dom'},
                                                   $oldd->{'web_sslport'});
my ($nonvirt, $nonvconf) = &get_apache_virtual($d->{'dom'},
						  $d->{'web_port'});
my $tmpl = &get_template($d->{'template'});

if ($d->{'ip'} ne $oldd->{'ip'} ||
    $d->{'ip6'} ne $oldd->{'ip6'} ||
    $d->{'virt6'} != $oldd->{'virt6'} ||
    $d->{'name6'} != $oldd->{'name6'} ||
    $d->{'web_sslport'} != $oldd->{'web_sslport'}) {
	# IP address or port has changed .. update VirtualHost
	&$first_print($text{'save_ssl'});
	if (!$virt) {
		&$second_print($text{'delete_noapache'});
		goto VIRTFAILED;
		}
	my ($nvstar, $nvstar6);
	if ($d->{'ip'}) {
		$nvstar = &add_name_virtual($d, $conf,
			$d->{'web_sslport'}, 0, $d->{'ip'});
		}
	if ($d->{'ip6'}) {
		$nvstar6 = &add_name_virtual(
			$d, $conf, $d->{'web_sslport'}, 0, $d->{'ip6'});
		}
	&add_listen($d, $conf, $d->{'web_sslport'});

	# Change the virtualhost IPs
	$virt->{'value'} = &get_apache_vhost_ips($d, $nvstar, $nvstar6,
						 $d->{'web_sslport'});
	&apache::save_directive_struct($virt, $virt, $conf, $conf, 1);
	&flush_file_lines($virt->{'file'});
	$rv++;
	&$second_print($text{'setup_done'});
	}
if ($d->{'home'} ne $oldd->{'home'}) {
	# Home directory has changed .. update any directives that referred
	# to the old directory
	&$first_print($text{'save_ssl3'});
	if (!$virt) {
		&$second_print($text{'delete_noapache'});
		goto VIRTFAILED;
		}
	&recursive_fix_apache_config(
		$vconf, $conf, $oldd->{'home'}, $d->{'home'});
	&flush_file_lines($virt->{'file'});
	$rv++;
	&$second_print($text{'setup_done'});
	}
if ($d->{'user'} ne $oldd->{'user'}) {
	# Username has changed .. copy suexec directives from parent
	&$first_print($text{'save_ssl10'});
	if (!$virt || !$nonvirt) {
		&$second_print($text{'delete_noapache'});
		goto VIRTFAILED;
		}
	my @vals = &apache::find_directive("SuexecUserGroup", $nonvconf);
	if (@vals) {
		&apache::save_directive(
			"SuexecUserGroup", \@vals, $vconf, $conf);
		&flush_file_lines($virt->{'file'});
		}
	$rv++;
	&$second_print($text{'setup_done'});
	}
if ($d->{'dom'} ne $oldd->{'dom'}) {
        # Domain name has changed .. update any directives that contain
        # the old domain name
        &$first_print($text{'save_ssl2'});
	if (!$virt) {
		&$second_print($text{'delete_noapache'});
		goto VIRTFAILED;
		}
	&modify_web_domain($d, $oldd, $virt, $vconf, $conf, 1);
        $rv++;
        &$second_print($text{'setup_done'});
        }

# Code after here still works even if SSL virtualhost is missing
VIRTFAILED:

# Update SSL cert linkage
&update_ssl_link_on_domain_change($d, $oldd);

if ($d->{'home'} ne $oldd->{'home'}) {
	# Fix SSL cert file locations
	foreach my $k ('ssl_cert', 'ssl_key', 'ssl_chain', 'ssl_combined',
		       'ssl_everything') {
		$d->{$k} =~ s/\Q$oldd->{'home'}\E\//$d->{'home'}\//;
		}
	}

# Re-generate or request cert if required due to a domain name change
$rv += &rerequest_cert_on_domain_change($d, $oldd);

# If anything has changed that would impact the per-domain SSL cert for
# another server like Postfix or Webmin, re-set it up as long as it is supported
# with the new settings
&update_ssl_certs_on_change($d, $oldd);

# Update DANE DNS records
&sync_domain_tlsa_records($d);

&release_lock_web($d);
&register_post_action(\&restart_apache, &ssl_needs_apache_restart()) if ($rv);
return $rv;
}

# delete_ssl(&domain)
# Deletes the SSL virtual server from the Apache config
sub delete_ssl
{
my ($d) = @_;

&require_apache();
&$first_print($text{'delete_ssl'});
&obtain_lock_web($d);
my $conf = &apache::get_config();

# Remove the custom Listen directive added for the domain, if any
&remove_listen($d, $conf, $d->{'web_sslport'} || $default_web_sslport);

# Remove the <virtualhost>
my ($virt, $vconf) = &get_apache_virtual($d->{'dom'},
			    $d->{'web_sslport'} || $default_web_sslport);
my $tmpl = &get_template($d->{'template'});
if ($virt) {
	&delete_web_virtual_server($virt, $conf);
	&$second_print($text{'setup_done'});
	&register_post_action(\&restart_apache, &ssl_needs_apache_restart());
	}
else {
	&$second_print($text{'delete_noapache'});
	}

# If any other domains were using this one's SSL cert or key, break the linkage
foreach my $od (&get_domain_by("ssl_same", $d->{'id'})) {
	&break_ssl_linkage($od, $d);
	&save_domain($od);
	}

# Update DANE DNS records
&sync_domain_tlsa_records($d, 2);

# If this domain was sharing a cert with another, forget about it now
if ($d->{'ssl_same'}) {
	delete($d->{'ssl_cert'});
	delete($d->{'ssl_key'});
	delete($d->{'ssl_chain'});
	delete($d->{'ssl_combined'});
	delete($d->{'ssl_everything'});
	delete($d->{'ssl_same'});
	}

&release_lock_web($d);
return 1;
}

# clone_ssl(&domain, &old-domain)
# Since the non-SSL website has already been cloned and modified, just copy
# its directives and add SSL-specific options.
sub clone_ssl
{
my ($d, $oldd) = @_;
my $tmpl = &get_template($d->{'template'});
&$first_print($text{'clone_ssl'});
my ($virt, $vconf, $conf) = &get_apache_virtual($d->{'dom'},
						$d->{'web_sslport'});
my ($ovirt, $ovconf) = &get_apache_virtual($oldd->{'dom'},
					   $oldd->{'web_sslport'});
if (!$ovirt) {
	&$second_print($text{'clone_webold'});
	return 0;
	}
if (!$virt) {
	&$second_print($text{'clone_webnew'});
	return 0;
	}

# Fix up all the Apache directives
&clone_web_domain($oldd, $d, $ovirt, $virt, $conf);

# Is the linked SSL cert still valid for the new domain? If not, break the
# linkage by copying over the cert.
if ($d->{'ssl_same'} && !&check_domain_certificate($d->{'dom'}, $d)) {
	my $oldsame = &get_domain($d->{'ssl_same'});
	&break_ssl_linkage($d, $oldsame);
	}

# If in FPM mode update the port as well
my $mode = &get_domain_php_mode($oldd);
if ($mode eq "fpm") {
	# Force port re-allocation
	delete($d->{'php_fpm_port'});
	&save_domain_php_mode($d, $mode);
	}

# Re-generate combined cert file in case cert changed
&sync_combined_ssl_cert($d);

&release_lock_web($d);
&$second_print($text{'setup_done'});
&register_post_action(\&restart_apache, &ssl_needs_apache_restart());
return 1;
}

# validate_ssl(&domain)
# Returns an error message if no SSL Apache virtual host exists, or if the
# cert files are missing.
sub validate_ssl
{
my ($d) = @_;
my ($virt, $vconf, $conf) = &get_apache_virtual(
				$d->{'dom'}, $d->{'web_sslport'});
return &text('validate_essl', "<tt>$d->{'dom'}</tt>") if (!$virt);

# Check IP addresses
if ($d->{'virt'}) {
	my $ipp = $d->{'ip'}.":".$d->{'web_sslport'};
	&indexof($ipp, @{$virt->{'words'}}) >= 0 ||
		return &text('validate_ewebip', $ipp);
	}
if ($d->{'virt6'}) {
	my $ipp = "[".$d->{'ip6'}."]:".$d->{'web_sslport'};
	&indexof($ipp, @{$virt->{'words'}}) >= 0 ||
		return &text('validate_ewebip6', $ipp);
	}

# Make sure cert file exists
my $cert = &apache::find_directive("SSLCertificateFile", $vconf, 1);
if (!$cert) {
	return &text('validate_esslcert');
	}
elsif (!-e $cert) {
	return &text('validate_esslcertfile', "<tt>$cert</tt>");
	}
elsif (-l $cert) {
	return &text('validate_esslcertlink', "<tt>$cert</tt>");
	}
elsif (&is_under_directory($d->{'home'}, $cert) &&
       !&readable_by_domain_user($d, $cert)) {
	return &text('validate_esslcertfile2', "<tt>$cert</tt>");
	}

# Make sure key exists
my $key = &apache::find_directive("SSLCertificateKeyFile", $vconf, 1);
if ($key) {
	if (!-e $key) {
		return &text('validate_esslkeyfile', "<tt>$key</tt>");
		}
	elsif (-l $key) {
		return &text('validate_esslkeylink', "<tt>$key</tt>");
		}
	elsif (&is_under_directory($d->{'home'}, $key) &&
	       !&readable_by_domain_user($d, $key)) {
		return &text('validate_esslkeyfile2', "<tt>$key</tt>");
		}
	}

# Make sure the cert is readable
my $info = &cert_info($d);
if (!$info || (!$info->{'cn'} && !@{$info->{'alt'} || []})) {
	return &text('validate_esslcertinfo', "<tt>$cert</tt>");
	}
my $err = &validate_cert_format($cert, 'cert');
if ($err) {
	return $err;
	}

# Check the key type
my $type = &get_ssl_key_type($key, $d->{'ssl_pass'});
$type || return &text('validate_esslkeytype', "<tt>$key</tt>");

# Make sure the cert and key match
my $certdata = &read_file_contents($cert);
my $keydata = &read_file_contents($key);
my $err = check_cert_key_match($certdata, $keydata);
if ($err) {
	return $err;
	}

# Make sure this domain or www.domain matches cert. Include aliases, because
# in some cases the alias may be the externally visible domain
my $match = 0;
foreach my $cd ($d, &get_domain_by("alias", $d->{'id'})) {
	$match++ if (&check_domain_certificate($cd->{'dom'}, $d));
	}
if (!$match) {
	return &text('validate_essldom',
		     "<tt>".$d->{'dom'}."</tt>",
		     "<tt>"."www.".$d->{'dom'}."</tt>",
		     join(", ", map { "<tt>$_</tt>" }
			            &list_domain_certificate($d)));
	}

# Make sure the cert isn't expired
if ($info && $info->{'notafter'} && !$d->{'disabled'}) {
	my $notafter = &parse_notafter_date($info->{'notafter'});
	if ($notafter < time()) {
		return &text('validate_esslexpired', &make_date($notafter));
		}
	}

# Make sure the CA matches the cert. Use the domain's CA cert whether it is
# in a separate chain file or bundled into the combined cert, so that combined
# certs are validated too and a chain-less cert is not mistaken for its own CA
my $cainfo = &get_domain_ca_cert_info($d);
if ($cainfo && !&self_signed_cert($d)) {
	if (!$cainfo->{'cn'}) {
		return &text('validate_esslcainfo',
		    "<tt>".($d->{'ssl_chain'} || $d->{'ssl_combined'})."</tt>");
		}
	if (&cert_issuer_ca_mismatch($info, $cainfo)) {
		return &text('validate_esslcamatch',
		    &cert_principal_name($cainfo->{'o'}, $cainfo->{'cn'}),
		    &cert_principal_name($info->{'issuer_o'},
					 $info->{'issuer_cn'}));
		}
	}
	
# If the <virtualhost> address uses a *, make sure that no other
# virtualhost uses the domain's IP
if ($virt->{'words'}->[0] =~ /^\*/ && $d->{'ip'}) {
	my ($ipclash, $ipclashv);
	VHOST: foreach my $ovirt (&apache::find_directive_struct(
				"VirtualHost", $conf)) {
		foreach my $v (@{$ovirt->{'words'}}) {
			if ($v =~ /^([^:]+)(:(\d+))?/i &&
			    ($1 eq $d->{'ip'}) &&
			    (!$3 || $3 == $d->{'web_port'})) {
				$ipclash = $ovirt;
				$ipclashv = $v;
				last VHOST;
				}
			}
		}
	if ($ipclash) {
		my $sn = &apache::find_directive(
			"ServerName", $ipclash->{'members'});
		return &text('validate_envstar', $virt->{'words'}->[0],
						 $ipclashv, $sn);
		}
	}

return undef;
}

# check_ssl_clash(&domain, [field])
# Returns 1 if an SSL Apache webserver already exists for some domain, or if
# port 443 on the domain's IP is in use by Webmin or Usermin
sub check_ssl_clash
{
my $tmpl = &get_template($_[0]->{'template'});
my $web_sslport = $tmpl->{'web_sslport'} || 443;
if (!$_[1] || $_[1] eq 'dom') {
	# Check for <virtualhost> clash by domain name
	my ($cvirt, $cconf) = &get_apache_virtual($_[0]->{'dom'},
						     $web_sslport);
	return 1 if ($cvirt);
	}
if (!$_[1] || $_[1] eq 'ip') {
	# Check for clash by IP and port with Webmin or Usermin
	my $err = &check_webmin_port_clash($_[0], $web_sslport);
	return $err if ($err);
	}
return 0;
}

# check_webmin_port_clash(&domain, port)
# Returns 1 if Webmin or Usermin is using some IP and port
sub check_webmin_port_clash
{
my ($d, $port) = @_;
foreign_require("webmin");
my @checks;
my %miniserv;
&get_miniserv_config(\%miniserv);
push(@checks, [ \%miniserv, "Webmin" ]);
if (&foreign_installed("usermin")) {
	my %uminiserv;
	foreign_require("usermin");
	&usermin::get_usermin_miniserv_config(\%uminiserv);
	push(@checks, [ \%uminiserv, "Usermin" ]);
	}
foreach my $c (@checks) {
	my @sockets = &webmin::get_miniserv_sockets($c->[0]);
	foreach my $s (@sockets) {
		if (($s->[0] eq '*' ||
		     ($d->{'ip'} && $s->[0] eq $d->{'ip'}) ||
		     ($d->{'ip6'} && $s->[0] eq $d->{'ip6'})) &&
		    $s->[1] == $port) {
			return &text('setup_esslportclash',
				     $d->{'ip'} || $d->{'ip6'},
				     $port, $c->[1]);
			}
		}
	}
return undef;
}

# disable_ssl(&domain)
# Adds a directive to force all requests to show an error page
sub disable_ssl
{
&$first_print($text{'disable_ssl'});
&require_apache();
my ($virt, $vconf) = &get_apache_virtual($_[0]->{'dom'},
					    $_[0]->{'web_sslport'});
if ($virt) {
        &create_disable_directives($virt, $vconf, $_[0]);
        &$second_print($text{'setup_done'});
	&register_post_action(\&restart_apache);
	return 1;
        }
else {
        &$second_print($text{'delete_noapache'});
	return 0;
        }
}

# enable_ssl(&domain)
sub enable_ssl
{
&$first_print($text{'enable_ssl'});
&require_apache();
my ($virt, $vconf) = &get_apache_virtual($_[0]->{'dom'},
					    $_[0]->{'web_sslport'});
if ($virt) {
        &remove_disable_directives($virt, $vconf, $_[0]);
        &$second_print($text{'setup_done'});
	&register_post_action(\&restart_apache);
	return 1;
        }
else {
        &$second_print($text{'delete_noapache'});
	return 0;
        }
}

# backup_ssl(&domain, file)
# Save the SSL virtual server's Apache config as a separate file
sub backup_ssl
{
my ($d, $file) = @_;
&$first_print($text{'backup_sslcp'});

# Save the apache directives
my ($virt, $vconf) = &get_apache_virtual($d->{'dom'}, $d->{'web_sslport'});
if ($virt) {
	my $lref = &read_file_lines($virt->{'file'});
	&open_tempfile_as_domain_user($d, FILE, ">$file");
	foreach my $l (@$lref[$virt->{'line'} .. $virt->{'eline'}]) {
		&print_tempfile(FILE, "$l\n");
		}
	&close_tempfile_as_domain_user($d, FILE);

	# Save the cert and key, if any
	my $cert = &get_website_ssl_file($d, "cert");
	if ($cert) {
		&copy_write_as_domain_user($d, $cert, $file."_cert");
		}
	my $key = &get_website_ssl_file($d, "key");
	if ($key && $key ne $cert) {
		&copy_write_as_domain_user($d, $key, $file."_key");
		}
	my $ca = &get_website_ssl_file($d, "ca");
	if ($ca) {
		&copy_write_as_domain_user($d, $ca, $file."_ca");
		}

	&$second_print($text{'setup_done'});
	return 1;
	}
else {
	&$second_print($text{'delete_noapache'});
	return 0;
	}
}

# restore_ssl(&domain, file, &options)
# Update the SSL virtual server's Apache configuration from a file. Does not
# change the actual <Virtualhost> lines!
sub restore_ssl
{
my ($d, $file, $opts, $allopts, $homefmt, $oldd) = @_;
&$first_print($text{'restore_sslcp'});
&obtain_lock_web($d);
my $rv = 1;

# Restore the Apache directives
my ($virt, $vconf, $conf) = &get_apache_virtual(
				$d->{'dom'}, $d->{'web_sslport'});
if ($virt) {
	my $srclref = &read_file_lines($file, 1);

	# Copy across directives from the backup
	my @mems = &apache_lines_to_config([ @$srclref[1 .. @$srclref-2] ]);
	my $newvirt = { 'name' => 'VirtualHost',
			'value' => $virt->{'value'},
			'type' => 1,
			'members' => \@mems };
	&apache::save_directive_struct($virt, $newvirt, $conf, $conf);
	$virt = $newvirt;
	$vconf = $virt->{'members'};

	# Fix up any DocumentRoot or other file-related directives
	if ($oldd->{'home'} && $oldd->{'home'} ne $d->{'home'}) {
		&recursive_fix_apache_config(
			$vconf, $conf, $oldd->{'home'}, $d->{'home'});
		}

	# Copy suexec-related directives from non-SSL virtual host
	my ($nvirt, $nvconf) = &get_apache_virtual($d->{'dom'},
						   $d->{'web_port'});
	if ($nvirt && $virt) {
		my @vals = &apache::find_directive("SuexecUserGroup", $nvconf);
		if (@vals) {
			&apache::save_directive("SuexecUserGroup", \@vals,
						$vconf, $conf);
			&flush_file_lines($virt->{'file'});
			}
		}
	&flush_file_lines($virt->{'file'}, undef, 1);

	if (!$d->{'ssl_same'}) {
		# Restore the cert and key, if any and if saved and if not
		# shared with another domain
		my $cert = &get_website_ssl_file($d, "cert");
		if ($cert && -r $file."_cert") {
			&lock_file($cert);
			&write_ssl_file_contents($d, $cert, $file."_cert");
			&unlock_file($cert);
			&save_website_ssl_file($d, "cert", $cert);
			}
		my $key = &get_website_ssl_file($d, "key");
		if ($key && -r $file."_key" && $key ne $cert) {
			&lock_file($key);
			&write_ssl_file_contents($d, $key, $file."_key");
			&unlock_file($key);
			&save_website_ssl_file($d, "key", $key);
			}
		my $ca = &get_website_ssl_file($d, "ca");
		if (-r $file."_ca") {
			# CA in the backup, so add one if needed
			if (!$ca) {
				$ca = &default_certificate_file($d, "ca");
				}
			&lock_file($ca);
			&write_ssl_file_contents($d, $ca, $file."_ca");
			&unlock_file($ca);
			}
		else {
			# No CA in the backup, so clear current setting
			$ca = undef;
			}
		&save_website_ssl_file($d, "ca", $ca);
		&refresh_ssl_cert_expiry($d);
		&sync_combined_ssl_cert($d);
		}
	else {
		# Make sure that the Apache config uses the correct SSL path
		&save_website_ssl_file($d, "cert", $d->{'ssl_cert'});
		&save_website_ssl_file($d, "key", $d->{'ssl_key'});
		&save_website_ssl_file($d, "ca", $d->{'ssl_chain'});
		}

	# Re-setup any SSL passphrase
	&save_domain_passphrase($d);

	# Re-save PHP mode, in case it changed
	&save_domain_php_mode($d, &get_domain_php_mode($d));

	# Re-save CGI mode from non-SSL domain
	&save_domain_cgi_mode($d, &get_domain_cgi_mode($d));

	# Add Require all granted directive if this system is Apache 2.4
	&add_require_all_granted_directives($d, $d->{'web_sslport'});

	# If the restored config contains php_value entires but this system
	# doesn't support mod_php, remove them
	&fix_mod_php_directives($d, $d->{'web_sslport'});

	# Fix Options lines
	&fix_options_directives($vconf, $conf, 0);

	# Handle case where there are DAV directives, but it isn't enabled
	&remove_dav_directives($d, $virt, $vconf, $conf);

	# Re-save CA cert path based on actual config
	if (!$d->{'ssl_same'}) {
		$d->{'ssl_chain'} = &get_website_ssl_file($d, 'ca');

		# Sync cert to Dovecot, Postfix, Webmin, etc..
		&enable_domain_service_ssl_certs($d);
		}

	&$second_print($text{'setup_done'});
	}
else {
	&$second_print($text{'delete_noapache'});
	$rv = 0;
	}

&release_lock_web($d);
&register_post_action(\&restart_apache);
return $rv;
}

# cert_info(&domain)
# Returns a hash of details of a domain's cert
sub cert_info
{
my ($d) = @_;
return &cert_file_info($d->{'ssl_cert'});
}

# cert_file_split(file|data)
# Returns a list of certs in some file
sub cert_file_split
{
my ($file) = @_;
my $data;
if ($file =~ /^\//) {
	$data = &read_file_contents($file);
	}
else {
	$data = $file;
	}
my @rv;
my @lines = split(/\r?\n/, $data);
foreach my $l (@lines) {
	my $cl = $l;
	$cl =~ s/^#.*//;
	if ($cl =~ /^-----BEGIN/) {
		push(@rv, $cl."\n");
		}
	elsif ($cl =~ /\S/ && @rv) {
		$rv[$#rv] .= $cl."\n";
		}
	}
return @rv;
}

# cert_file_info_openssl(file)
# Returns certificate fields parsed with openssl.
sub cert_file_info_openssl
{
my ($file) = @_;
return undef if (!$file || !-r $file);
# Use -text instead of -ext subjectAltName for OpenSSL 1.0.x compatibility.
my $out = &backquote_command("openssl x509 -noout -nameopt RFC2253 ".
	"-issuer -subject -startdate -enddate -text ".
	"-in ".quotemeta($file)." 2>/dev/null");
return undef if ($?);
my %rv;
my $parse_name = sub {
	my ($name, $prefix) = @_;
	foreach my $part (split(/(?<!\\),/, $name)) {
		my ($k, $v) = split(/=/, $part, 2);
		next if (!defined($k) || !defined($v));
		$v =~ s/\\([,=+<>#;\\" ])/$1/g;
		my $kp = $prefix ? $prefix."_" : "";
		$rv{$kp."c"} = $v if ($k eq "C");
		$rv{$kp."o"} = $v if ($k eq "O");
		$rv{$kp."cn"} = $v if ($k eq "CN");
		}
	};
my @alts;
my $in_san = 0;
my $san_indent = 0;
foreach my $l (split(/\r?\n/, $out)) {
	if ($l =~ /^issuer\s*=\s*(.*)$/) {
		$parse_name->($1, "issuer");
		}
	elsif ($l =~ /^subject\s*=\s*(.*)$/) {
		$parse_name->($1, "");
		}
	elsif ($l =~ /^notBefore\s*=\s*(.*)$/) {
		$rv{'notbefore'} = $1;
		}
	elsif ($l =~ /^notAfter\s*=\s*(.*)$/) {
		$rv{'notafter'} = $1;
		}
	if ($l =~ /^(\s*)X509v3 Subject Alternative Name:\s*(.*)$/) {
		$in_san = 1;
		$san_indent = length($1);
		$l = $2;
		}
	elsif ($in_san) {
		# OpenSSL indents extension values deeper than extension
		# headers, including private OID headers we do not know by name.
		my $continuation_indent = $san_indent + 1;
		if ($l !~ /^\s{$continuation_indent,}\S/) {
			$in_san = 0;
			}
		}
	if ($in_san) {
		while ($l =~ /(?:^\s*|,\s*)DNS:([^,\s]+)/g) {
			push(@alts, $1);
			}
		while ($l =~ /(?:^\s*|,\s*)IP Address:([^,\s]+)/g) {
			push(@alts, &normalize_ssl_cert_identifier($1));
			}
		}
	}
$rv{'alt'} = [ &unique(@alts) ] if (@alts);
$rv{'self'} = $rv{'cn'} && $rv{'issuer_cn'} &&
	      $rv{'cn'} eq $rv{'issuer_cn'} &&
	      (!$rv{'o'} || !$rv{'issuer_o'} || $rv{'o'} eq $rv{'issuer_o'})
	      ? 1 : 0;
return \%rv;
}

# cert_data_info(data)
# Returns details of a cert in PEM text format
sub cert_data_info
{
my ($data) = @_;
my $temp = &transname();
&open_tempfile(TEMP, ">$temp", 0, 1);
&print_tempfile(TEMP, $data);
&close_tempfile(TEMP);
my $info = &cert_file_info($temp);
&unlink_file($temp);
return $info;
}

# get_domain_ca_cert_info(&domain)
# Returns a cert_file_info hash for the CA (chain) cert used by a domain,
# whether it is stored in a separate CA file or bundled into the combined
# cert file after the leaf. Returns undef if the domain has no CA cert.
sub get_domain_ca_cert_info
{
my ($d) = @_;
# Use the separate CA / chain file if the domain has one
if ($d->{'ssl_chain'} && -r $d->{'ssl_chain'}) {
	return &cert_file_info($d->{'ssl_chain'});
	}
# Otherwise the CA cert may be bundled into the combined cert file, as the
# cert following the leaf
if ($d->{'ssl_combined'} && -r $d->{'ssl_combined'}) {
	my @certs = &cert_file_split($d->{'ssl_combined'});
	return &cert_data_info($certs[1]) if (@certs > 1);
	}
return undef;
}


# cert_file_info_perl(file)
# Returns a hash ref with info about the cert in file, or undef on error using
# pure Perl
sub cert_file_info_perl
{
my ($file) = @_;
return undef if (!$file || !-r $file);

my ($cert, $pkey, $x509, %rv);

# Cleanup and return
my $shutdown = sub {
	Net::SSLeay::EVP_PKEY_free($pkey) if $pkey; # Free pkey before returning
	Net::SSLeay::X509_free($cert) if $cert;     # Free cert before returning
	return undef;
};

# Always expect PEM
my $bio = Net::SSLeay::BIO_new_file($file, 'r') or return undef;
$cert = Net::SSLeay::PEM_read_bio_X509($bio);
Net::SSLeay::BIO_free($bio);
return undef if !$cert;

# Get issuer
my $issuer = Net::SSLeay::X509_get_issuer_name($cert);
if ($issuer) {
	my $C  = Net::SSLeay::X509_NAME_get_text_by_NID($issuer, 14);
	my $O  = Net::SSLeay::X509_NAME_get_text_by_NID($issuer, 17);
	my $CN = Net::SSLeay::X509_NAME_get_text_by_NID($issuer, 13);
	$rv{issuer_c}  = $C  if defined $C  && $C  ne '' && $C  ne '-1';
	$rv{issuer_o}  = $O  if defined $O  && $O  ne '' && $O  ne '-1';
	$rv{issuer_cn} = $CN if defined $CN && $CN ne '' && $CN ne '-1';
	}

# Validity period begin
my $nb = Net::SSLeay::X509_get_notBefore($cert);
if ($nb) {
	my $s = Net::SSLeay::P_ASN1_TIME_put2string($nb);
	$rv{notbefore} = $s if $s;
	}

# Validity period end
my $na = Net::SSLeay::X509_get_notAfter($cert);
if ($na) {
	my $s = Net::SSLeay::P_ASN1_TIME_put2string($na);
	$rv{notafter} = $s if $s;
	}

# Subject
my $subject = Net::SSLeay::X509_get_subject_name($cert);
if ($subject) {
	my $O     = Net::SSLeay::X509_NAME_get_text_by_NID($subject, 17);
	my $CN    = Net::SSLeay::X509_NAME_get_text_by_NID($subject, 13);
	my $Email = Net::SSLeay::X509_NAME_get_text_by_NID($subject, 48);
	$rv{o}     = $O     if defined $O     && $O     ne '' && $O     ne '-1';
	$rv{cn}    = $CN    if defined $CN    && $CN    ne '' && $CN    ne '-1';
	$rv{email} = $Email if defined $Email && $Email ne '' && $Email ne '-1';
	}

# Public key
$pkey = Net::SSLeay::X509_get_pubkey($cert);
return $shutdown->() if (!$pkey);
my $type_nid = Net::SSLeay::EVP_PKEY_id($pkey);
my $type_sn  = Net::SSLeay::OBJ_nid2sn($type_nid);
return $shutdown->() if (!$type_sn);
my $algo = $type_sn =~ /rsa/                      ? 'rsa'     :
	   $type_sn =~ /^(?:ec|id-ecpublickey)$/i ? 'ec'      :
	   $type_sn =~ /ed25519/                  ? 'ed25519' : undef;

$rv{algo} = $algo;
return $shutdown->() if !$algo;	# unsupported algo

# Key size
my $size = Net::SSLeay::EVP_PKEY_bits($pkey);
$rv{size} = "$size" if $size && $size > 0;

# Modulus and exponent parsing SPKI manually
{
	# Manually parse one ASN.1 DER tag-length-value structure from data,
	# starting at a given offset; this is equivalent to how low-level
	# decoders in Convert::ASN1 modules unpack
	my $tlv = sub {
		my ($data, $iref) = @_;
		
		# Initialize cursor
		my $i = $$iref // 0;
		return if $i >= length $data;

		# Need at least one byte for the tag
		my $tag = ord substr($data, $i++, 1);
		return if $i >= length $data;

		# Need at least one byte for the length
		my $len = ord substr($data, $i++, 1);

		# If high bit is set, long-form length follows, DER forbids 0x80
		# (indefinite length), so n must be >= 1
		if ($len & 0x80) {
			my $n = $len & 0x7F; # how many bytes follow
			return if $n == 0;   # 0x80 (indef.) not allowed in DER
			return if $i + $n > length $data; # bounds check

			# Parse big-endian length across n bytes
			$len = 0;
			for (1..$n) { $len = ($len << 8) + 
				      ord substr($data, $i++, 1) }
			}

		# Value must fit in remaining buffer
		return if $i + $len > length $data;

		# Slice out the value, then advance cursor past it
		my $val = substr($data, $i, $len);
		$i += $len;

		# Publish new cursor
		$$iref = $i;

		# Return the tag byte and the raw value bytes
		return ($tag, $val);
	};

	# Decode a DER-encoded object identifier into a dotted-decimal
	# string; follows the same base-128 decoding rules used in
	# Convert::ASN1 and when interpreting OID values
	my $oid_from_der = sub {
		my ($s) = @_;
		# Split to bytes; must have at least one byte for the combined
		# (a0,a1)
		my @b = unpack 'C*', $s;
		return unless @b;

		# First byte encodes a0 and a1: x = 40*a0 + a1
		my $first = shift @b;
		my $a0 = int($first / 40);
		my $a1 = $first - 40 * $a0;
		my @arcs = ($a0, $a1);

		# Decode remaining arcs as base-128 varints. v accumulates 7-bit
		# chunks until we hit a byte with MSB=0 (end of arc)
		my $v = 0;
		for my $c (@b) {
			$v = ($v << 7) | ($c & 0x7F);
			if (($c & 0x80) == 0) {
				push @arcs, $v; $v = 0
				}
			}
		return join '.', @arcs;
	};

	# Map OIDs to human-readable short names (curve names)
	my $oid_sn = sub {
		my ($oid) = @_;
		return {
			'1.2.840.10045.3.1.7'  => 'prime256v1',
			'1.3.132.0.33'         => 'secp224r1',
			'1.3.132.0.34'         => 'secp384r1',
			'1.3.132.0.35'         => 'secp521r1',
			 # Note: an algorithm OID, not a curve param
			'1.3.101.112'          => 'ed25519',
		}->{$oid};
	};

	# From the full cert DER, extract subjectPublicKey bytes and curve OID.
	# Basically a low-level DER walker to extract the BIT STRING of the
	# subjectPublicKey and the named curve OID in case of EC keys; similar
	# to Crypt::OpenSSL::X509 internals
	my $find_spki_bytes_and_curve = sub {
		my ($der) = @_;

		# Parse outer certificate SEQUENCE
		my $i = 0;
		my ($t_cert, $v_cert) = $tlv->($der, \$i) or return;
		return if $t_cert != 0x30;

		# Parse tbsCertificate SEQUENCE
		my $j = 0;
		my ($t_tbs, $v_tbs) = $tlv->($v_cert, \$j) or return;
		return if $t_tbs != 0x30;

		# Optional version field [0] EXPLICIT
		my $k = 0;
		my $tag = $k < length($v_tbs)
			? ord substr($v_tbs, $k, 1)
			: undef;
		$tlv->($v_tbs, \$k) if (defined $tag && ($tag & 0xE0) == 0xA0);

		# Skip fixed serial, signature, issuer, validity and subject
		for (1..5) { $tlv->($v_tbs, \$k) or return }

		# subjectPublicKeyInfo ::= SEQUENCE
		my ($t_spki, $v_spki) = $tlv->($v_tbs, \$k) or return;
		return if $t_spki != 0x30;

		# AlgorithmIdentifier ::= SEQUENCE
		my $m = 0;
		my ($t_ai, $v_ai) = $tlv->($v_spki, \$m) or return;
		return if $t_ai != 0x30;

		# Algorithm OID
		my $n = 0;
		my ($t_oid, $v_oid) = $tlv->($v_ai, \$n) or return;
		return if $t_oid != 0x06;

		# Optional curve OID as algorithm parameter
		my $curve_oid;
		if ($n < length $v_ai) {
			my ($t_par, $v_par) = $tlv->($v_ai, \$n) or return;
			$curve_oid = $oid_from_der->($v_par) if $t_par == 0x06;
			}

		# subjectPublicKey BIT STRING drop the unused bits prefix
		my ($t_spk, $v_spk) = $tlv->($v_spki, \$m) or return;
		return if $t_spk != 0x03;

		substr($v_spk, 0, 1, '');	# drop unused-bits byte
		return ($v_spk, $curve_oid);
	};

	# Get the PEM text from the already-loaded cert, decode to DER
	my $pem = Net::SSLeay::PEM_get_string_X509($cert)
		or return $shutdown->();

	# Extract the base64 part of the PEM
	$pem =~ /
	  (-----BEGIN\s+CERTIFICATE-----\n
	   (?<pem>[A-Za-z0-9+\/=\n\r]+)
	   -----END\s+CERTIFICATE-----) /sx or return $shutdown->();
	my $der = &decode_base64($+{pem});
	return $shutdown->() if !$der;

	# Find SPKI
	my ($spk_bytes, $curve_oid) = $find_spki_bytes_and_curve->($der)
		or return $shutdown->();
	
	# Parse SPKI based on algorithm
	if ($algo eq 'rsa') {
		# RSAPublicKey SEQUENCE { n INTEGER, e INTEGER }
		my $r = 0;
		my ($t_seq, $v_seq) = $tlv->($spk_bytes, \$r)
			or return $shutdown->();

		# Expect a SEQUENCE tag (0x30) at the start of RSAPublicKey
		return $shutdown->() if $t_seq != 0x30;

		# Parse modulus and exponent
		my $s = 0;
		my ($t_n, $v_n) = $tlv->($v_seq, \$s) or return $shutdown->();
		my ($t_e, $v_e) = $tlv->($v_seq, \$s) or return $shutdown->();

		# Both modulus and exponent must be INTEGER tags (0x02)
		return $shutdown->() if $t_n != 0x02 || $t_e != 0x02;

		# Convert modulus to colon-separated hex
		my $mod_hex = unpack 'H*', $v_n;
		$mod_hex = "0$mod_hex" if length($mod_hex) % 2;	# byte-align
		$rv{modulus} = join(':', unpack '(A2)*', lc $mod_hex);

		# Convert exponent to decimal string
		my $e_hex = unpack('H*', $v_e);
		my $e_dec = hex($e_hex);
		$rv{exponent} = "$e_dec" if $e_dec > 0;
		}
	elsif ($algo eq 'ec') {
		# Bit string value is the uncompressed point
		my $pub = unpack('H*', $spk_bytes);
		$pub = "0$pub" if length($pub) % 2;     # byte-align
		$rv{modulus} = join(':', unpack '(A2)*', lc($pub));

		if ($curve_oid) {
			# Map curve short names to standard NIST names
			my %nist = ( prime256v1 => 'P-256',
				     secp224r1  => 'P-224',
				     secp384r1  => 'P-384',
				     secp521r1  => 'P-521' );
			my $curve_sn = $oid_sn->($curve_oid) || $curve_oid;

			# Build the exponent field to show both the OID and NIST
			# name
			my @p = ( "ASN1 OID: $curve_sn" );
			push @p,  "NIST CURVE: $nist{$curve_sn}"
				if exists $nist{$curve_sn};
			$rv{exponent} = join(', ', @p);
			}
		}
}

# Subject alt names
my @alts = Net::SSLeay::X509_get_subjectAltNames($cert);
if (@alts) {
	my @dns;
	while (my ($type, $val) = splice(@alts, 0, 2)) {
		push @dns, $val if $type == 2;	# dNSName
		push @dns, &ssl_cert_packed_ip_address($val)
			if $type == 7;		# iPAddress
		}
	$rv{alt} = \@dns if @dns;
	}

# Self-signed or not
$rv{self} = $subject && $issuer && 
	    Net::SSLeay::X509_NAME_cmp($subject, $issuer) == 0 ? 1 : 0;

# Cleanup
$shutdown->();

# Return info
return \%rv;
}

# cert_file_info(file)
# Returns a hash of details of a cert in some file
sub cert_file_info
{
my $file = shift;

# Read cert info using Perl modules, and fall back to openssl for fields that
# older Net::SSLeay versions may leave blank on SAN-only certificates.
my $info = &cert_file_info_perl($file);
if (!$info || !$info->{'issuer_cn'} || !$info->{'notafter'} ||
    (!$info->{'cn'} && !@{$info->{'alt'} || []})) {
	my $oinfo = &cert_file_info_openssl($file);
	if (!$info) {
		$info = $oinfo;
		}
	elsif ($oinfo) {
		foreach my $k (qw(c o issuer_c issuer_o issuer_cn notbefore
				  notafter self)) {
			$info->{$k} = $oinfo->{$k}
				if ((!defined($info->{$k}) || $info->{$k} eq "") &&
				    defined($oinfo->{$k}));
			}
		if (@{$oinfo->{'alt'} || []}) {
			my %seen;
			$info->{'alt'} = [
				grep {
					my $k = &normalize_ssl_cert_identifier($_);
					!$seen{$k}++;
					} (@{$info->{'alt'} || []},
					   @{$oinfo->{'alt'}})
				];
			}
		}
	}
return undef if (!$info);

# Type string
$info->{type} = $info->{self} ? $text{cert_typeself} : $text{cert_typereal};
return $info;
}

# cert_principal_name(organization, common-name)
# Formats a cert's organization and common name as "O/CN", or just "CN" when
# there is no organization (as on Let's Encrypt leaf certs)
sub cert_principal_name
{
my ($o, $cn) = @_;
return $o ? "$o/$cn" : $cn;
}

# cert_issuer_ca_mismatch(&cert-info, &ca-info)
# Returns 1 only when the certificate issuer and CA subject are known to differ.
sub cert_issuer_ca_mismatch
{
my ($info, $cainfo) = @_;
return 0 if (!$info || !$cainfo || !$info->{'issuer_cn'} || !$cainfo->{'cn'});
return 1 if ($info->{'issuer_cn'} ne $cainfo->{'cn'});
# Some SAN-only certificates can omit issuer fields from the parsed data.
# Treat organization as a mismatch only when both sides provide it.
return 1 if ($info->{'issuer_o'} && $cainfo->{'o'} &&
	     $info->{'issuer_o'} ne $cainfo->{'o'});
return 0;
}

# convert_ssl_key_format(&domain, file, "pkcs1"|"pkcs8", [outfile])
# Convert an SSL key into a different format
sub convert_ssl_key_format
{
my ($d, $file, $fmt, $outfile) = @_;
$outfile ||= $file;
my $cmd;
if ($fmt eq "pkcs1") {
	$cmd = "openssl rsa -in ".quotemeta($file)." -out ".quotemeta($outfile);
	}
elsif ($fmt eq "pkcs8") {
	$cmd = "openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in ".
	       quotemeta($file)." -out ".quotemeta($outfile);
	}
else {
	return "Unknown format $fmt";
	}
if ($d && &is_under_directory($d->{'home'}, $file)) {
	$cmd = &command_as_user($d->{'user'}, 0, $cmd);
	}
my $out = &backquote_logged("$cmd 2>&1 </dev/null");
return $? ? $out : undef;
}

# parse_notafter_date(str)
# Parse a date string like "Nov 30 07:46:00 2016 GMT" into a Unix time
sub parse_notafter_date
{
my ($str) = @_;
&foreign_require("mailboxes");
return &mailboxes::parse_mail_date($str);
}

# same_cert_file(file1, file2)
# Checks if the certs in some files are the same. This means either the
# same file, or the same modulus and expiry date.
sub same_cert_file
{
my ($file1, $file2) = @_;
return 1 if (!$file1 && !$file2);
return 0 if ($file1 && !$file2 || !$file1 && $file2);
return 1 if (&same_file($file1, $file2));
my $info1 = &cert_file_info($file1);
my $info2 = &cert_file_info($file2);
return 1 if (!$info1 && !$info2);
return 0 if ($info1 && !$info2 || !$info1 && $info2);
return $info1->{'modulus'} && $info2->{'modulus'} &&
       $info1->{'modulus'} eq $info2->{'modulus'} &&
       $info1->{'notafter'} eq $info2->{'notafter'};
}

# same_cert_file_any(file1, file2)
# Checks if the modulus and expiry of the cert in file1 are the same as those
# for any of the certs in file2.
sub same_cert_file_any
{
my ($file1, $file2) = @_;
return 1 if (!$file1 && !$file2);
return 0 if ($file1 && !$file2 || !$file1 && $file2);
return 1 if (&same_file($file1, $file2));
my $info1 = &cert_file_info($file1);
foreach my $sp (&cert_file_split($file2)) {
	my $info2 = &cert_data_info($sp);
	return 1 if ($info1->{'modulus'} && $info2->{'modulus'} &&
		     $info1->{'modulus'} eq $info2->{'modulus'} &&
		     $info1->{'notafter'} eq $info2->{'notafter'});
	}
return 0;
}

# get_ssl_key_type(file, [passphrase])
# Returns 'rsa' or 'ec' depending on the key type
sub get_ssl_key_type
{
my ($key, $pass) = @_;

# First check if it's in the key file format
my $lref = &read_file_lines($key, 1);
foreach my $l (@$lref) {
	if ($l =~ /-----BEGIN\s+(RSA|EC)\s+PRIVATE\s+KEY----/) {
		return lc($1);
		}
	}

# Fallback to seeing if the openssl command can parse it
my $qpass = $pass ? quotemeta($pass) : "NONE";
foreach my $t ('rsa', 'ec') {
	my $ex = &execute_command("openssl $t -in ".quotemeta($key).
				  " -text -passin pass:".$qpass);
	return $t if (!$ex);
	}

return undef;
}

# check_passphrase(key-data, passphrase)
# Returns 0 if a passphrase is needed by not given, 1 if not needed, 2 if OK
sub check_passphrase
{
my ($newkey, $pass) = @_;
my $temp = &transname();
&open_tempfile(KEY, ">$temp", 0, 1);
&set_ownership_permissions(undef, undef, 0700, $temp);
&print_tempfile(KEY, $newkey);
&close_tempfile(KEY);
my $type = &get_ssl_key_type($temp, $pass);
my $rv = &execute_command("openssl $type -in ".quotemeta($temp).
			     " -text -passin pass:NONE");
if (!$rv) {
	return 1;
	}
if ($pass) {
	my $rv = &execute_command("openssl $type -in ".quotemeta($temp).
				     " -text -passin pass:".quotemeta($pass));
	if (!$rv) {
		return 2;
		}
	}
return 0;
}

# get_key_size(file)
# Given an SSL key file, returns the size in bits
sub get_key_size
{
my ($file) = @_;
my $type = &get_ssl_key_type($file);
my $out = &backquote_command(
	"openssl $type -in ".quotemeta($file)." -text 2>&1 </dev/null");
if ($out =~ /Private-Key:\s+\((\d+)/i) {
	return $1;
	}
return undef;
}

# save_domain_passphrase(&domain)
# Configure Apache to use the right passphrase for a domain, if one is needed.
# Otherwise, remove the passphrase config.
sub save_domain_passphrase
{
my ($d) = @_;
my $p = &domain_has_website($d);
if ($p ne "web") {
	return &plugin_call($p, "feature_save_web_passphrase", $d);
	}
my $pass_script = "$ssl_passphrase_dir/$d->{'id'}";
&lock_file($pass_script);
my ($virt, $vconf, $conf) = &get_apache_virtual($d->{'dom'},
                                                   $d->{'web_sslport'});
return "SSL virtual host not found" if (!$vconf);
my @pps = &apache::find_directive("SSLPassPhraseDialog", $conf);
my @pps_str = &apache::find_directive_struct("SSLPassPhraseDialog", $conf);
&lock_file(@pps_str ? $pps_str[0]->{'file'} : $conf->[0]->{'file'});
my ($pps) = grep { $_ eq "exec:$pass_script" } @pps;
if ($d->{'ssl_pass'}) {
	# Create script, add to Apache config
	if (!-d $ssl_passphrase_dir) {
		&make_dir($ssl_passphrase_dir, 0700);
		}
	&open_tempfile(SCRIPT, ">$pass_script");
	&print_tempfile(SCRIPT, "#!/bin/sh\n");
	&print_tempfile(SCRIPT, "echo ".quotemeta($d->{'ssl_pass'})."\n");
	&close_tempfile(SCRIPT);
	&set_ownership_permissions(undef, undef, 0700, $pass_script);
	push(@pps, "exec:$pass_script");
	}
else {
	# Remove script and from Apache config
	if ($pps) {
		@pps = grep { $_ ne $pps } @pps;
		}
	&unlink_file($pass_script);
	}
my $pps_file = @pps_str ? $pps_str[0]->{'file'} : $conf->[0]->{'file'};
&lock_file($pps_file);
&apache::save_directive("SSLPassPhraseDialog", \@pps, $conf, $conf);
&flush_file_lines();
&unlock_file($pps_file);
&register_post_action(\&restart_apache, &ssl_needs_apache_restart());
}

# check_cert_key_match(cert-text, key-text)
# Checks if the modulus for a cert and key match and are valid. Returns undef 
# on success or an error message on failure.
sub check_cert_key_match
{
my ($certtext, $keytext) = @_;
my $certfile = &transname();
my $keyfile = &transname();
foreach my $tf ([ $certtext, $certfile ], [ $keytext, $keyfile ]) {
	&open_tempfile(CERTOUT, ">$tf->[1]", 0, 1);
	&print_tempfile(CERTOUT, $tf->[0]);
	&close_tempfile(CERTOUT);
	}
my $type = &get_ssl_key_type($keyfile);

if ($type eq "ec") {
	# Get the public key data from the cert
	my $x;
	my $certpub = &extract_public_key($x=&backquote_command(
		"openssl x509 -noout -text -in ".quotemeta($certfile)." 2>&1"));
	my $keypub = &extract_public_key($x=&backquote_command(
		"openssl ec -noout -text -in ".quotemeta($keyfile)." 2>&1"));
	$certpub eq $keypub ||
		return "Certificate and private key do not match";
	}
else {
	# Get certificate modulus
	my $certmodout = &backquote_command(
	    "openssl x509 -noout -modulus -in ".quotemeta($certfile)." 2>&1");
	$certmodout =~ /Modulus=([A-F0-9]+)/i ||
		return "Certificate data is not valid : $certmodout";
	my $certmod = $1;

	# Get key modulus
	my $keymodout = &backquote_command(
	    "openssl $type -noout -modulus -in ".quotemeta($keyfile)." 2>&1");
	$keymodout =~ /Modulus=([A-F0-9]+)/i ||
		return "Key data is not valid : $keymodout";
	my $keymod = $1;

	# Make sure they match
	$certmod eq $keymod ||
		return "Certificate and private key do not match";
	}

return undef;
}

# extract_public_key(text)
# Given openssl -text output, extract the public key data
sub extract_public_key
{
my ($txt) = @_;
my $found = 0;
my $pub = "";
foreach my $l (split(/\r?\n/, $txt)) {
	if ($l =~ /^\s*pub:\s*$/) {
		$found = 1;
		}
	elsif ($l =~ /^\s+([a-f0-9:]+)\s*$/ && $found) {
		$pub .= $1;
		}
	elsif ($found) {
		last;
		}
	}
return $pub;
}

# validate_cert_format(data|file, type)
# Checks if some file or string contains valid cert or key data, and returns
# an error message if not. The type can be one of 'key', 'cert', 'ca' or 'csr'
sub validate_cert_format
{
my ($data, $type) = @_;
if ($data =~ /^\//) {
	$data = &read_file_contents($data);
	}
my %headers = ( 'key' => '(RSA |EC )?PRIVATE KEY',
		   'cert' => '(CERTIFICATE|PUBLIC KEY)',
		   'ca' => '(CERTIFICATE|PUBLIC KEY)',
		   'csr' => 'CERTIFICATE REQUEST',
		   'newkey' => '(RSA |EC )?PRIVATE KEY' );
my $h = $headers{$type};
$h || return "Unknown SSL file type $type";
($data) = &extract_cert_parameters($data);
my @lines = grep { /\S/ } split(/\r?\n/, $data);
my $begin = quotemeta("-----BEGIN ").$h.quotemeta("-----");
my $end = quotemeta("-----END ").$h.quotemeta("-----");
@lines || return "Data is <em>empty</em>, but expected -----BEGIN $h-----";
$lines[0] =~ /^$begin$/ ||
	return "Data starts with $lines[0] , but expected -----BEGIN $h-----";
$lines[$#lines] =~ /^$end$/ ||
	return "Data ends with $lines[$#lines] , but expected -----END $h-----";
for(my $i=1; $i<$#lines; $i++) {
	$lines[$i] =~ /^[A-Za-z0-9\+\/=]+\s*$/ ||
	$lines[$i] =~ /^$begin$/ ||
	$lines[$i] =~ /^$end$/ ||
		return "Line ".($i+1)." does not look like PEM format";
	}
@lines > 4 || return "Data only has ".scalar(@lines)." lines";
return undef;
}

# extract_cert_parameters(cert-text)
# Given a cert text that might contain a -----BEGIN EC PARAMETERS----- block,
# return the rest of the file and that block if it exists
sub extract_cert_parameters
{
my ($data) = @_;
my @parts = &cert_file_split($data);
my $rv = "";
my $params = "";
foreach my $p (@parts) {
	if ($p =~ /^-----BEGIN\s+(\S+)\s+PARAMETERS-----/) {
		$params .= $p;
		}
	else {
		$rv .= $p;
		}
	}
return ($rv, $params);
}

# cert_pem_data(&domain)
# Returns a domain's cert in PEM format
sub cert_pem_data
{
my ($d) = @_;
my $data;
if (&is_under_directory($d->{'home'}, $d->{'ssl_cert'})) {
	$data = &read_file_contents_as_domain_user($d, $d->{'ssl_cert'});
	}
else {
	$data = &read_file_contents($d->{'ssl_cert'});
	}
$data =~ s/\r//g;
if ($data =~ /(-----BEGIN\s+CERTIFICATE-----\n([A-Za-z0-9\+\/=\n\r]+)-----END\s+CERTIFICATE-----)/) {
	return $1;
	}
return undef;
}

# key_pem_data(&domain)
# Returns a domain's key in PEM format
sub key_pem_data
{
my ($d) = @_;
my $file = $d->{'ssl_key'} || $d->{'ssl_cert'};
my $data;
if (&is_under_directory($d->{'home'}, $file)) {
	$data = &read_file_contents_as_domain_user($d, $file);
	}
else {
	$data = &read_file_contents($file);
	}
$data =~ s/\r//g;
if ($data =~ /(-----BEGIN\s+RSA\s+PRIVATE\s+KEY-----\n([A-Za-z0-9\+\/=\n\r]+)-----END\s+RSA\s+PRIVATE\s+KEY-----)/) {
	return $1;
	}
elsif ($data =~ /(-----BEGIN\s+PRIVATE\s+KEY-----\n([A-Za-z0-9\+\/=\n\r]+)-----END\s+PRIVATE\s+KEY-----)/) {
	return $1;
	}
elsif ($data =~ /(-----BEGIN\s+EC\s+PRIVATE\s+KEY-----\n([A-Za-z0-9\+\/=\n\r]+)-----END\s+EC\s+PRIVATE\s+KEY-----)/) {
	return $1;
	}
return undef;
}

# cert_pkcs12_data(&domain)
# Returns a domain's cert in PKCS12 format
sub cert_pkcs12_data
{
my ($d) = @_;
my $cmd = "openssl pkcs12 -in ".quotemeta($d->{'ssl_cert'}).
             " -inkey ".quotemeta($_[0]->{'ssl_key'}).
	     " -export -passout pass: -nokeys";
open(OUT, &command_as_user($d->{'user'}, 0, $cmd)." |");
while(<OUT>) {
	$data .= $_;
	}
close(OUT);
return $data;
}

# key_pkcs12_data(&domain)
# Returns a domain's key in PKCS12 format
sub key_pkcs12_data
{
my ($d) = @_;
my $cmd = "openssl pkcs12 -in ".quotemeta($d->{'ssl_cert'}).
             " -inkey ".quotemeta($_[0]->{'ssl_key'}).
	     " -export -passout pass: -nocerts";
open(OUT, &command_as_user($d->{'user'}, 0, $cmd)." |");
while(<OUT>) {
	$data .= $_;
	}
close(OUT);
return $data;
}

# domain_miniserv_ssl_ips(&domain)
# Returns IPs that should map to this domain's cert in miniserv.
sub domain_miniserv_ssl_ips
{
my ($d) = @_;
my @ips;
if ($d->{'virt'}) {
	push(@ips, &normalize_ssl_cert_identifier($d->{'ip'}));
	}
if ($d->{'virt6'}) {
	push(@ips, &normalize_ssl_cert_identifier($d->{'ip6'}));
	}

my @names;
my $info = &cert_info($d);
if ($info) {
	push(@names, $info->{'cn'}, @{$info->{'alt'} || []});
	}
foreach my $ip (grep { &ssl_cert_identifier_is_ip($_) } @names) {
	# For shared-IP domains, only the default website can own the IP
	# certificate. Include both the public DNS IP and the local vhost IP,
	# plus IPv4-mapped form because miniserv may receive that as SNI.
	my $v6 = &check_ip6address($ip);
	my $certip = $v6 ? ($d->{'dns_ip6'} || $d->{'ip6'})
			 : ($d->{'dns_ip'} || $d->{'ip'});
	my $localip = $v6 ? $d->{'ip6'} : $d->{'ip'};
	next if (!$certip);
	next if (&normalize_ssl_cert_identifier($ip) ne
		 &normalize_ssl_cert_identifier($certip));
	my $virt = $v6 ? $d->{'virt6'} : $d->{'virt'};
	if (!$virt) {
		# Never let an arbitrary shared-IP certificate installation
		# take over miniserv's IP mapping. The ACME request path checks
		# this too, but the miniserv write path must enforce ownership.
		next if (!&domain_has_website($d) || !&is_default_website($d));
		}
	push(@ips, map { &normalize_ssl_cert_identifier($_) }
		   grep { $_ } ($localip, $certip));
	if (!$v6) {
		foreach my $mapip (&unique(grep { $_ } ($localip, $certip))) {
			push(@ips, &normalize_ssl_cert_identifier(
					"::ffff:$mapip"));
			}
		}
	}
return &unique(grep { $_ } @ips);
}

# miniserv_ipkey_has_any(&ipkey, &values)
# Returns 1 if a miniserv ipkey has any of some values.
sub miniserv_ipkey_has_any
{
my ($ipkey, $values) = @_;
foreach my $v (@$values) {
	foreach my $k (@{$ipkey->{'ips'}}) {
		if (&ssl_cert_identifier_is_ip($v) ||
		    &ssl_cert_identifier_is_ip($k)) {
			return 1 if (&normalize_ssl_cert_identifier($v) eq
				     &normalize_ssl_cert_identifier($k));
			}
		else {
			return 1 if (lc($v) eq lc($k));
			}
		}
	}
return 0;
}

# miniserv_ipkey_matches_domain(&domain, &ipkey)
# Returns 1 if a miniserv ipkey belongs to some domain.
sub miniserv_ipkey_matches_domain
{
my ($d, $ipkey) = @_;
my @doms = ( $d, &get_domain_by("alias", $d->{'id'}) );
my @dnames = map { ($_->{'dom'}, "*.".$_->{'dom'}) } @doms;
return 1 if (&miniserv_ipkey_has_any($ipkey, \@dnames));
return 1 if (&miniserv_ipkey_has_any($ipkey,
				     [ &domain_miniserv_ssl_ips($d) ]));
return 0;
}

# setup_ipkeys(&domain, &miniserv-getter, &miniserv-saver, &post-action)
# Add the per-IP/domain SSL key for some domain
sub setup_ipkeys
{
my ($d, $getfunc, $putfunc, $postfunc) = @_;
my @doms = ( $d, &get_domain_by("alias", $d->{'id'}) );
my @dnames = map { ($_->{'dom'}, "*.".$_->{'dom'}) } @doms;
&foreign_require("webmin");
my %miniserv;
&$getfunc(\%miniserv);
my @ipkeys = grep { !&miniserv_ipkey_matches_domain($d, $_) }
		     &webmin::get_ipkeys(\%miniserv);
my @ips = &domain_miniserv_ssl_ips($d);
push(@ips, @dnames);
my $chain = &get_website_ssl_file($d, 'ca');
push(@ipkeys, { 'ips' => \@ips,
		'key' => $d->{'ssl_key'},
		'cert' => $d->{'ssl_cert'},
		'extracas' => $chain, });
&webmin::save_ipkeys(\%miniserv, \@ipkeys);
&$putfunc(\%miniserv);
&register_post_action($postfunc);
return 1;
}

# delete_ipkeys(&domain, &miniserv-getter, &miniserv-saver, &post-action)
# Remove the per-IP/domain SSL key for some domain
sub delete_ipkeys
{
my ($d, $getfunc, $putfunc, $postfunc) = @_;
&foreign_require("webmin");
my %miniserv;
&$getfunc(\%miniserv);
my @ipkeys = &webmin::get_ipkeys(\%miniserv);
my @newipkeys;
foreach my $ipk (@ipkeys) {
	my $del = &miniserv_ipkey_matches_domain($d, $ipk);
	if (!$del) {
		push(@newipkeys, $ipk);
		}
	}
if (@ipkeys != @newipkeys) {
	# Some change was found to apply
	&webmin::save_ipkeys(\%miniserv, \@newipkeys);
	&$putfunc(\%miniserv);
	&register_post_action($postfunc);
	return undef;
	}
return $text{'delete_esslnoips'};
}

# apache_combined_cert(&domain)
# Returns 1 if Apache should be pointed to the combined SSL cert file
sub apache_combined_cert
{
my ($d) = @_;
my $tmpl = &get_template($d->{'template'});
&require_apache();
if ($tmpl->{'ssl_combined_cert'} == 2) {
	return 1;
	}
elsif ($tmpl->{'ssl_combined_cert'} == 1) {
	return 0;
	}
else {
	return &compare_versions($apache::httpd_modules{'core'}, "2.4.6") >= 0;
	}
}

# apache_ssl_directives(&domain, template)
# Returns extra Apache directives needed for SSL
sub apache_ssl_directives
{
my ($d, $tmpl) = @_;
&require_apache();
my @dirs;
push(@dirs, "SSLEngine on");
if (&apache_combined_cert($d)) {
	push(@dirs, "SSLCertificateFile $d->{'ssl_combined'}");
	}
else {
	push(@dirs, "SSLCertificateFile $d->{'ssl_cert'}");
	if ($d->{'ssl_chain'}) {
		push(@dirs, "SSLCACertificateFile $d->{'ssl_chain'}");
		}
	}
push(@dirs, "SSLCertificateKeyFile $d->{'ssl_key'}");
if ($tmpl->{'web_sslprotos'}) {
	push(@dirs, "SSLProtocol ".$tmpl->{'web_sslprotos'});
	}
else {
	my @tls = ( "SSLv2", "SSLv3" );
	if ($apache::httpd_modules{'core'} >= 2.4) {
		push(@tls, "TLSv1");
		if (&compare_version_numbers(&get_openssl_version(), '>=', '1.0.0')) {
			push(@tls, "TLSv1.1");
			}
		}
	push(@dirs, "SSLProtocol ".join(" ", "all", map { "-".$_ } @tls));
	}
if ($tmpl->{'web_ssl'} ne 'none') {
	my $ssl_dirs = $tmpl->{'web_ssl'};
	$ssl_dirs =~ s/\t/\n/g;
	$ssl_dirs = &substitute_domain_template($ssl_dirs, $d);
	push(@dirs, split(/\n/, $ssl_dirs));
	}
return @dirs;
}

# check_certificate_data(data)
# Checks if some data looks like a valid cert. Returns undef if OK, or an error
# message if not
sub check_certificate_data
{
my ($data) = @_;
my @lines = split(/\r?\n/, $data);
my @certs;
my $inside = 0;
foreach my $l (@lines) {
	if ($l =~ /^-+BEGIN/) {
		push(@certs, $l."\n");
		$inside = 1;
		}
	elsif ($l =~ /^-+END/) {
		$inside || return $text{'cert_eoutside'};
		$certs[$#certs] .= $l."\n";
		$inside = 0;
		}
	elsif ($inside) {
		$certs[$#certs] .= $l."\n";
		}
	}
$inside && return $text{'cert_einside'};
@certs || return $text{'cert_ecerts'};
my $temp = &transname();
foreach my $cdata (@certs) {
	&open_tempfile(CERTDATA, ">$temp", 0, 1);
	&print_tempfile(CERTDATA, $cdata);
	&close_tempfile(CERTDATA);
	my $out = &backquote_command("openssl x509 -in ".quotemeta($temp)." -issuer -subject -enddate 2>&1");
	my $ex = $?;
	&unlink_file($temp);
	if ($ex) {
		return "<tt>".&html_escape($out)."</tt>";
		}
	elsif ($out !~ /subject\s*=\s*.*(CN|O)\s*=/) {
		return $text{'cert_esubject'};
		}
	}
return undef;
}

# default_certificate_file(&domain, "cert"|"key"|"ca"|"combined"|"everything")
# Returns the default path that should be used for a cert, key or CA file
sub default_certificate_file
{
my ($d, $mode) = @_;
$mode = "ca" if ($mode eq "chain");
my $tmpl = &get_template($d->{'template'});
my $file = $tmpl->{'cert_'.$mode.'_tmpl'};
if ($file eq "auto" && $mode ne "key") {
	# Path is relative to the key file
	my $keyfile = $tmpl->{'cert_key_tmpl'};
	if ($keyfile && $keyfile =~ s/\/[^\/]+$//) {
		$file = $keyfile."/ssl.".$mode;
		}
	else {
		$file = undef;
		}
	}
return $file ? &absolute_domain_path($d, &substitute_domain_template($file, $d))
	     : "$d->{'home'}/ssl.".$mode;
}

# relative_certificate_file(file, type)
# Returns a cert path in the same directory as the given file, but of a
# different type.
sub relative_certificate_file
{
my ($file, $mode) = @_;
$mode = "ca" if ($mode eq "chain");
$file =~ s/\/[^\/]+$//;
return $file."/ssl.".$mode;
}

# set_certificate_permissions(&domain, file)
# Set permissions on a cert file so that Apache can read them.
sub set_certificate_permissions
{
my ($d, $file) = @_;
if (&is_under_directory($d->{'home'}, $file)) {
	&set_permissions_as_domain_user($d, 0700, $file);
	}
else {
	&set_ownership_permissions(undef, undef, 0700, $file);
	}
}

# check_domain_certificate(domain-name, &domain-with-cert|&cert-info)
# Returns 1 if some virtual server's certificate can be used for a particular
# domain, 0 if not. Based on the common names, including wildcards and UCC
sub check_domain_certificate
{
my ($dname, $d_or_info) = @_;
my $info = $d_or_info->{'dom'} ? &cert_info($d_or_info) : $d_or_info;
my $is_ip = &ssl_cert_identifier_is_ip($dname);
foreach my $check ($is_ip ? ($dname) : ($dname, "www.".$dname)) {
	my $ncheck = &normalize_ssl_cert_identifier($check);
	if (&normalize_ssl_cert_identifier($info->{'cn'}) eq $ncheck) {
		# Exact match
		return 1;
		}
	elsif (!$is_ip && $info->{'cn'} =~ /^\*\.(\S+)$/ &&
	       (lc($check) eq lc($1) || $check =~ /^([^\.]+)\.\Q$1\E$/i)) {
		# Matches wildcard
		return 1;
		}
	elsif ($info->{'cn'} eq '*') {
		# Cert is for * , which matches everything
		return 1;
		}
	else {
		# Check for subjectAltNames match (as seen in UCC certs)
		foreach my $a (@{$info->{'alt'} || []}) {
			if (&normalize_ssl_cert_identifier($a) eq $ncheck ||
			    !$is_ip && $a =~ /^\*\.(\S+)$/ &&
			    (lc($check) eq lc($1) ||
			     $check =~ /([^\.]+)\.\Q$1\E$/i)) {
				return 1;
				}
			}
		}
	}
return 0;
}

# list_domain_certificate(&domain|&cert-info)
# Returns a list of DNS names and IP addresses that are in the cert
sub list_domain_certificate
{
my ($d_or_info) = @_;
my $info = $d_or_info->{'dom'} ? &cert_info($d_or_info) : $d_or_info;
my @rv;
push(@rv, $info->{'cn'}) if ($info->{'cn'});
push(@rv, @{$info->{'alt'}}) if ($info->{'alt'});
return &unique(@rv);
}

# self_signed_cert(&domain)
# Returns 1 if some domain has a self-signed certificate
sub self_signed_cert
{
my ($d) = @_;
my $info = &cert_info($d);
return &self_signed_cert_info($info);
}

# self_signed_cert_info(&info)
# Returns 1 if some cert is self-signed
sub self_signed_cert_info
{
my ($info) = @_;
return $info->{'issuer_cn'} eq $info->{'cn'} &&
       $info->{'issuer_o'} eq $info->{'o'};
}

# find_openssl_config_file()
# Returns the full path to the OpenSSL config file, or undef if not found
sub find_openssl_config_file
{
&foreign_require("webmin");
return &webmin::find_openssl_config_file();
}

# generate_self_signed_cert(certfile, keyfile, size, days, country, state,
# 			    city, org, orgunit, commonname, email, &altnames,
# 			    &domain, [cert-type])
# Generates a new self-signed cert, and stores it in the given cert and key
# files. Returns undef on success, or an error message on failure.
sub generate_self_signed_cert
{
my ($certfile, $keyfile, $size, $days, $country, $state, $city, $org,
       $orgunit, $common, $email, $altnames, $d, $ctype) = @_;
my $tmpl = &get_template($d->{'template'});
$ctype ||= $tmpl->{'ssl_cert_type'};
&foreign_require("webmin");
$size ||= $webmin::default_key_size;
$days ||= 1825;

# Prepare for SSL alt names
my @cnames = ( $common );
push(@cnames, @$altnames) if ($altnames);
my $conf = &webmin::build_ssl_config(\@cnames);
my $subject = &webmin::build_ssl_subject($country, $state, $city, $org,
					 $orgunit, \@cnames, $email);

# Call openssl and write to temp files
my $keytemp = &transname();
my $certtemp = &transname();
my $ctypeflag = $ctype eq "sha2" || $ctype =~ /^ec/ ? "-sha256" : "";
my $addtextsup = &compare_version_numbers(&get_openssl_version(), '>=', '1.1.1') ? "-addext extendedKeyUsage=serverAuth" : "";
my $out;
if ($ctype =~ /^ec/) {
	my $pubtemp = &transname();
	$out = &backquote_logged(
		"openssl ecparam -genkey -name prime256v1 ".
		"-noout -out ".quotemeta($keytemp)." 2>&1 && ".
		"openssl ec -in ".quotemeta($keytemp)." -pubout ".
		"-out ".quotemeta($pubtemp)." 2>&1 && ".
		"openssl req -new -x509 -reqexts v3_req -days $days ".
		"-config ".quotemeta($conf)." -subj ".quotemeta($subject).
		" $addtextsup -utf8 -key ".quotemeta($keytemp)." ".
		"-out ".quotemeta($certtemp)." 2>&1");
	}
else {
	$out = &backquote_logged(
		"openssl req $ctypeflag -reqexts v3_req -newkey rsa:$size ".
		"-x509 -nodes -out ".quotemeta($certtemp).
		" -keyout ".quotemeta($keytemp)." ".
		"-days $days -config ".quotemeta($conf).
		" -subj ".quotemeta($subject)." $addtextsup -utf8 2>&1");
	}
my $rv = $?;
if (!-s $certtemp || !-s $keytemp || $rv) {
	# Failed .. return error
	return &text('csr_ekey', "<pre>$out</pre>");
	}
my $certdata = &read_file_contents($certtemp);
my $keydata = &read_file_contents($keytemp);
my $err = &validate_cert_format($certdata, 'cert');
return &text('csr_ekey', "<pre>$err</pre>") if ($err);
$err = &validate_cert_format($keydata, 'key');
return &text('csr_ekey', "<pre>$err</pre>") if ($err);
$err = &check_cert_key_match($certdata, $keydata);
return &text('csr_ekey', "<pre>$err</pre>") if ($err);

# Save as domain owner
&create_ssl_certificate_directories($d);
my $werr = &write_ssl_file_contents($d, $certfile, $certdata, 1);
return $werr if ($werr);
$werr = &write_ssl_file_contents($d, $keyfile, $keydata, 1);
return $werr if ($werr);
my $fcertdata = &read_file_contents($certfile);
my $fkeydata = &read_file_contents($keyfile);
$err = &validate_cert_format($fcertdata, 'cert');
return &text('csr_ekey', "<pre>$err</pre>") if ($err);
$err = &validate_cert_format($fkeydata, 'key');
return &text('csr_ekey', "<pre>$err</pre>") if ($err);
$err = &check_cert_key_match($fcertdata, $fkeydata);
return &text('csr_ekey', "<pre>$err</pre>") if ($err);
&sync_combined_ssl_cert($d);

return undef;
}

# generate_certificate_request(csrfile, keyfile, size, country, state,
# 			       city, org, orgunit, commonname, email, &altnames,
# 			       &domain, [cert-type])
# Generates a new self-signed cert, and stores it in the given csr and key
# files. Returns undef on success, or an error message on failure.
sub generate_certificate_request
{
my ($csrfile, $keyfile, $size, $country, $state, $city, $org,
       $orgunit, $common, $email, $altnames, $d, $ctype) = @_;
my $tmpl = &get_template($d->{'template'});
$ctype ||= $tmpl->{'ssl_cert_type'};
&foreign_require("webmin");
$size ||= $webmin::default_key_size;

# Generate the key
my $keytemp = &transname();
my $out;
if ($ctype =~ /^ec/) {
	$out = &backquote_command(
		"openssl ecparam -genkey -name prime256v1 -out ".
		quotemeta($keytemp)." 2>&1 </dev/null");
	}
else {
	$out = &backquote_command(
		"openssl genrsa -out ".quotemeta($keytemp).
		" $size 2>&1 </dev/null");
	}
my $rv = $?;
if (!-s $keytemp || $rv) {
	return &text('csr_ekey', "<pre>$out</pre>");
	}

# Generate the CSR
my @cnames = ( $common );
push(@cnames, @$altnames) if ($altnames);
my ($ok, $csrtemp) = &webmin::generate_ssl_csr($keytemp, $country, $state, $city, $org, $orgunit, \@cnames, $email, $ctype);
if (!$ok) {
	return &text('csr_ecsr', "<pre>$csrtemp</pre>");
	}

# Copy into place
&create_ssl_certificate_directories($d);
my $werr = &write_ssl_file_contents($d, $keyfile,
				    &read_file_contents($keytemp), 1);
return $werr if ($werr);
$werr = &write_ssl_file_contents($d, $csrfile,
				 &read_file_contents($csrtemp), 1);
return $werr if ($werr);
return undef;
}

# obtain_lock_ssl(&domain)
# Lock the Apache config file for some domain, and the Webmin config
sub obtain_lock_ssl
{
my ($d) = @_;
return if (!$config{'ssl'});
&obtain_lock_anything($d);
&obtain_lock_web($d) if ($d->{'web'});
if ($main::got_lock_ssl == 0) {
	my @sfiles = ($ENV{'MINISERV_CONFIG'} ||
		         "$config_directory/miniserv.conf",
		        $config_directory =~ /^(.*)\/webmin$/ ?
		         "$1/usermin/miniserv.conf" :
			 "/etc/usermin/miniserv.conf");
	foreach my $k ('ssl_cert', 'ssl_key', 'ssl_chain') {
		push(@sfiles, $d->{$k}) if ($d->{$k});
		}
	@sfiles = &unique(@sfiles);
	foreach my $f (@sfiles) {
		&lock_file($f);
		}
	@main::got_lock_ssl_files = @sfiles;
	}
$main::got_lock_ssl++;
}

# release_lock_web(&domain)
# Un-lock the Apache config file for some domain, and the Webmin config
sub release_lock_ssl
{
my ($d) = @_;
return if (!$config{'ssl'});
&release_lock_web($d) if ($d->{'web'});
if ($main::got_lock_ssl == 1) {
	foreach my $f (@main::got_lock_ssl_files) {
		&unlock_file($f);
		}
	}
$main::got_lock_ssl-- if ($main::got_lock_ssl);
&release_lock_anything($d);
}

# find_matching_certificate_domain(&domain)
# Check if another domain on the same IP already has a matching cert, and if so
# return it (or a list of matches)
sub find_matching_certificate_domain
{
my ($d) = @_;
my $ipkey = $d->{'ip'} ? 'ip' : 'ip6';
my $ip = $d->{$ipkey};
return wantarray ? ( ) : undef if (!$ip);
my @sslclashes = grep { $_->{$ipkey} && $_->{$ipkey} eq $ip &&
			   &domain_has_ssl($_) &&
			   $_->{'id'} ne $d->{'id'} &&
			   !$_->{'ssl_same'} } &list_domains();
my @rv;
foreach my $sslclash (@sslclashes) {
	if (&check_domain_certificate($d->{'dom'}, $sslclash)) {
		push(@rv, $sslclash);
		}
	}
return wantarray ? @rv : $rv[0];
}

# find_matching_certificate(&domain)
# For a domain with SSL being enabled, check if another domain on the same IP
# with the same owner already has a matching cert. If so, update the domain
# hash's cert file. This can only be called at domain creation time.
sub find_matching_certificate
{
my ($d) = @_;
my $lnk = $d->{'link_certs'} == 1 ? 1 :
	  $d->{'link_certs'} == 2 ? 2 :
	  $d->{'nolink_certs'} ? 0 :
	  $config{'nolink_certs'} == 1 ? 0 :
	  $config{'nolink_certs'} == 2 ? 2 : 1;
if ($lnk) {
	my @sames = &find_matching_certificate_domain($d);
	if ($lnk != 2) {
		@sames = grep { $_->{'user'} eq $d->{'user'} } @sames;
		}
	if (@sames) {
		my ($same) = grep { !$_->{'parent'} } @sames;
		$same ||= $sames[0];
		if ($same) {
			# Found a match, so add a link to it
			&link_matching_certificate($d, $same, 0);
			}
		}
	}
}

# link_matching_certificate(&domain, &samedomain, [save-actual-config])
# Makes the first domain use SSL cert file owned by the second
sub link_matching_certificate
{
my ($d, $sslclash, $save) = @_;
my @beforecerts = &get_all_domain_service_ssl_certs($d);
$d->{'ssl_cert'} = $sslclash->{'ssl_cert'};
$d->{'ssl_key'} = $sslclash->{'ssl_key'};
$d->{'ssl_same'} = $sslclash->{'id'};
$d->{'ssl_chain'} = &get_website_ssl_file($sslclash, 'ca');
if ($save) {
	&save_website_ssl_file($d, 'cert', $d->{'ssl_cert'});
	&save_website_ssl_file($d, 'key', $d->{'ssl_key'});
	&save_website_ssl_file($d, 'ca', $d->{'ssl_chain'});
	&sync_combined_ssl_cert($d);
	&update_all_domain_service_ssl_certs($d, \@beforecerts);
	}
}

# generate_default_certificate(&domain)
# If a domain doesn't have a cert file set, pick one and generate a self-signed
# cert if needed. May print stuff.
sub generate_default_certificate
{
my ($d) = @_;
$d->{'ssl_key'} ||= &default_certificate_file($d, 'key');
$d->{'ssl_cert'} ||= &default_certificate_file($d, 'cert');
if (!-s $d->{'ssl_cert'} && !-s $d->{'ssl_key'}) {
	# Need to do it
	my $temp = &transname();
	&$first_print($text{'setup_openssl'});
	&lock_file($d->{'ssl_cert'});
	&lock_file($d->{'ssl_key'});
	my @alts = ( $d->{'dom'}, "localhost",
			&get_system_hostname(0),
			&get_system_hostname(1) );
	@alts = &unique(@alts);
	my $err = &generate_self_signed_cert(
		$d->{'ssl_cert'}, $d->{'ssl_key'}, undef, 1825,
		undef, undef, undef,
		$d->{'default_cert_owner'} ? $d->{'owner'} : undef,
		undef,
		"*.$d->{'dom'}",
		$d->{'default_cert_owner'} ? $d->{'emailto_addr'} : undef,
		\@alts, $d);
	if ($err) {
		&$second_print(&text('setup_eopenssl', $err));
		&unlock_file($d->{'ssl_cert'});
		&unlock_file($d->{'ssl_key'});
		return 0;
		}
	else {
		&set_certificate_permissions($d, $d->{'ssl_cert'});
		&set_certificate_permissions($d, $d->{'ssl_key'});
		&$second_print($text{'setup_done'});
		}
	&unlock_file($d->{'ssl_cert'});
	&unlock_file($d->{'ssl_key'});
	delete($d->{'ssl_chain'});	# No longer valid
	&sync_combined_ssl_cert($d);
	return 1;
	}
return 0;
}

sub list_ssl_file_types
{
return ('key', 'cert', 'chain', 'combined', 'everything');
}

# move_website_ssl_file(&domain, type, new-file)
# Move an SSL cert or key file to a new location
sub move_website_ssl_file
{
my ($d, $type, $file) = @_;
my $oldfile = &get_website_ssl_file($d, $type);
return 0 if (!$oldfile || $oldfile eq $file);
&create_ssl_certificate_directories($d);
&write_ssl_file_contents($d, $file, $oldfile);
&lock_file($file);
&save_website_ssl_file($d, $type, $file);
&unlock_file($file);
foreach my $sd (&get_domain_by("ssl_same", $d->{'id'})) {
	&save_website_ssl_file($sd, $type, $file);
	}
&lock_file($oldfile);
if (&is_under_directory($d->{'home'}, $oldfile)) {
	&unlink_file_as_domain_user($d, $oldfile);
	}
else {
	&unlink_file($oldfile);
	}
&unlock_file($oldfile);
return 1;
}

# break_ssl_linkage(&domain, &old-same-domain)
# If domain was using the SSL cert from old-same-domain before, break the link
# by copying the cert into the default location for domain and updating the
# domain and Apache config to match
sub break_ssl_linkage
{
my ($d, $samed) = @_;
my @beforecerts = &get_all_domain_service_ssl_certs($d);

# Copy the cert and key to the new owning domain's directory. The guard
# checks the source domain's files, because the linked domain's own fields
# may be empty even when the owner has the file - in particular ssl_chain is
# never set on the linked domain when the owner uses a combined cert. Without
# copying it the rebuilt combined cert would be missing the CA chain, which
# triggers a bogus "CA cert does not match the issuer" validation error.
&create_ssl_certificate_directories($d);
foreach my $k (&list_ssl_file_types()) {
	if ($samed->{'ssl_'.$k}) {
		$d->{'ssl_'.$k} = &default_certificate_file($d, $k);
		&write_ssl_file_contents(
			$d, $d->{'ssl_'.$k}, $samed->{'ssl_'.$k});
		}
	}
delete($d->{'ssl_same'});

# Re-generate any combined cert files
&sync_combined_ssl_cert($d);

# Update webserver config
my $p = &domain_has_website($d);
if ($p) {
	if ($p eq 'web' && &apache_combined_cert($d)) {
		&save_website_ssl_file($d, "cert", $d->{'ssl_combined'});
		&save_website_ssl_file($d, "ca", undef);
		}
	else {
		&save_website_ssl_file($d, "cert", $d->{'ssl_cert'});
		&save_website_ssl_file($d, "ca", $d->{'ssl_chain'});
		}
	&save_website_ssl_file($d, "key", $d->{'ssl_key'});
	}

# Update any service certs for this domain
&update_all_domain_service_ssl_certs($d, \@beforecerts);

# If Let's Encrypt was in use before, copy across renewal fields
$d->{'letsencrypt_renew'} = $samed->{'letsencrypt_renew'};
$d->{'letsencrypt_last'} = $samed->{'letsencrypt_last'};
$d->{'letsencrypt_last_success'} = $samed->{'letsencrypt_last_success'};
$d->{'letsencrypt_last_failure'} = $samed->{'letsencrypt_last_failure'};
$d->{'letsencrypt_first_failure'} = $samed->{'letsencrypt_first_failure'};
$d->{'letsencrypt_last_err'} = $samed->{'letsencrypt_last_err'};
$d->{'ssl_cert_expiry'} = $samed->{'ssl_cert_expiry'} if ($samed->{'ssl_cert_expiry'});
}

# break_invalid_ssl_linkages(&domain, [&new-cert])
# Find all domains that link to this domain's SSL cert, and if their domain
# names are no longer legit for the cert, break the link.
sub break_invalid_ssl_linkages
{
my ($d, $newcert) = @_;
foreach $od (&get_domain_by("ssl_same", $d->{'id'})) {
	if (!&check_domain_certificate($od->{'dom'}, $newcert || $d)) {
		&obtain_lock_ssl($d);
		&break_ssl_linkage($od, $d);
		&save_domain($od);
		&release_lock_ssl($d);
		}
	}
}

# disable_letsencrypt_renewal(&domain)
# If Let's Encrypt renewal is enabled for a domain, turn it off on
# the assumption that a new cert type has been installed
sub disable_letsencrypt_renewal
{
my ($d) = @_;
if ($d->{'letsencrypt_renew'} || $d->{'letsencrypt_last_id'}) {
	delete($d->{'letsencrypt_renew'});
	delete($d->{'letsencrypt_last_id'});
	&save_domain($d);
	}
}

# hostname_under_domain(&domain|&domains, hostname)
# Returns 1 if some hostname belongs to a domain, and not any subdomain
sub hostname_under_domain
{
my ($d, $name) = @_;
if (ref($d) eq 'ARRAY') {
	# Under any of the domains?
	foreach my $dd (@$d) {
		return 1 if (&hostname_under_domain($dd, $name));
		}
	return 0;
	}
else {
	$name =~ s/\.$//;	# In case DNS record
	if ($name eq $d->{'dom'} ||
	    $name eq "*.".$d->{'dom'} ||
	    $name eq ".".$d->{'dom'}) {
		return 1;
		}
	elsif ($name =~ /^([^\.]+)\.(\S+)$/ && $2 eq $d->{'dom'}) {
		# Under the domain, but what if another domain owns it?
		my $o = &get_domain_by("dom", $name);
		return $o ? 0 : 1;
		}
	else {
		return 0;
		}
	}
}

# dovecot_local_name_cmp(name1, name2, dovecot-version)
# Returns -1 if name1 should be before name2 for Dovecot SNI matching,
# 1 if it should be after, or 0 if their relative order does not matter.
sub dovecot_local_name_cmp
{
my ($a, $b, $ver) = @_;
$a = lc($a);
$b = lc($b);
$a =~ s/\.$//;
$b =~ s/\.$//;
my $aw = $a =~ s/^\*\.// ? 1 : 0;
my $bw = $b =~ s/^\*\.// ? 1 : 0;
my $rv = $aw && !$bw && $b =~ /^[^\.]+\.\Q$a\E$/ ? -1 :
	 $bw && !$aw && $a =~ /^[^\.]+\.\Q$b\E$/ ? 1 : 0;
return &compare_versions($ver, "2.4") < 0 ? -$rv : $rv;
}

# sync_dovecot_ssl_cert(&domain, [enable-or-disable])
# If supported, configure Dovecot to use this domain's SSL cert for its IP
sub sync_dovecot_ssl_cert
{
my ($d, $enable) = @_;
my $tmpl = &get_template($d->{'template'});

# Check if dovecot is installed and supports this feature
return -1 if (!&foreign_installed("dovecot"));
&foreign_require("dovecot");
my $ver = &dovecot::get_dovecot_version();
return -1 if ($ver < 2);

undef(@dovecot::get_config_cache); 
my $cfile = &dovecot::get_config_file();
&lock_file($cfile);

# Check if dovecot is using SSL globally
my $conf = &dovecot::get_config();
my $sslyn = &dovecot::find_value("ssl", $conf);
if ($sslyn !~ /yes|required/i) {
	&unlock_file($cfile);
	return 0;
	}
my $ssldis = &dovecot::find_value("ssl_disable", $conf);
if ($ssldis =~ /yes/i) {
	&unlock_file($cfile);
	return 0;
	}

# Created combined file if needed
if (!$d->{'ssl_combined'} && !-r $d->{'ssl_combined'}) {
	&sync_combined_ssl_cert($d);
	}

my $chain = &get_website_ssl_file($d, "ca");
my $nochange = 0;

my @ips;
if ($d->{'virt'}) {
	# Domain has it's own IPv4, and maybe v6
	push(@ips, $d->{'ip'}) if ($d->{'ip'});
	push(@ips, "[".$d->{'ip6'}."]") if ($d->{'virt6'} && $d->{'ip6'});
	}
my ($cname, $cvalue) = &get_dovecot_ssl_dir("cert", $d->{'ssl_combined'});
my ($kname, $kvalue) = &get_dovecot_ssl_dir("key", $d->{'ssl_key'});
foreach my $ip (@ips) {
	# Find the existing block for the IP
	my @loc = grep { $_->{'name'} eq 'local' &&
			 $_->{'section'} } @$conf;
	my ($l) = grep { $_->{'value'} eq $ip } @loc;
	my ($imap, $pop3);
	if ($l) {
		($imap) = grep { $_->{'name'} eq 'protocol' &&
				 $_->{'value'} eq 'imap' &&
				 $_->{'enabled'} &&
				 $_->{'sectionname'} eq 'local' &&
				 $_->{'sectionvalue'} eq $ip } @$conf;
		($pop3) = grep { $_->{'name'} eq 'protocol' &&
				 $_->{'value'} eq 'pop3' &&
				 $_->{'enabled'} &&
				 $_->{'sectionname'} eq 'local' &&
				 $_->{'sectionvalue'} eq $ip } @$conf;
		}

	if ($enable) {
		# Remove any conflicting local_name blocks for this
		# domain before creating the local IP block. Dedicated-IP
		# domains should use local {} not local_name {}.
		# Ref: https://github.com/virtualmin/virtualmin-gpl/issues/1134
		my @lnloc = grep { $_->{'name'} eq 'local_name' &&
				   $_->{'section'} } @$conf;
		my @doms = ( $d, &get_domain_by("alias", $d->{'id'}) );
		my @conflicting = grep {
			&hostname_under_domain(\@doms, $_->{'value'})
			} @lnloc;
		foreach my $cl (@conflicting) {
			&dovecot::delete_section($conf, $cl);
			@$conf = grep { $_ ne $cl } @$conf;
			@$conf = grep {
				$_->{'sectionname'} ne $cl->{'name'} ||
				$_->{'sectionvalue'} ne $cl->{'value'}
				} @$conf;
			&flush_file_lines($cl->{'file'});
			}

		# Needs a cert for the IP
		if (!$l) {
			$l = { 'name' => 'local',
			       'value' => $ip,
			       'enabled' => 1,
			       'section' => 1,
			       'members' => [],
			       'file' => $cfile };
			&dovecot::create_section($conf, $l);
			push(@$conf, $l);
			&flush_file_lines($l->{'file'}, undef, 1);
			}
		if (!$imap) {
			$imap = { 'name' => 'protocol',
				  'value' => 'imap',
				  'members' => [
					{ 'name' => $cname,
					  'value' => $cvalue, },
					{ 'name' => $kname,
					  'value' => $kvalue, },
					],
				  'indent' => 1,
				  'enabled' => 1,
				  'sectionname' => 'local',
				  'sectionvalue' => $ip,
				  'file' => $l->{'file'} };
			&dovecot::create_section($conf, $imap, $l);
			push(@{$l->{'members'}}, $imap);
			push(@$conf, $imap);
			$l->{'eline'} = $imap->{'eline'}+1;
			}
		else {
			&dovecot::save_directive($imap->{'members'}, $cname, $cvalue);
			&dovecot::save_directive($imap->{'members'}, $kname, $kvalue);
			}
		if (!$pop3) {
			$pop3 = { 'name' => 'protocol',
				  'value' => 'pop3',
				  'members' => [
					{ 'name' => $cname,
					  'value' => $cvalue, },
					{ 'name' => $kname,
					  'value' => $kvalue, },
					],
				  'indent' => 1,
				  'enabled' => 1,
				  'sectionname' => 'local',
				  'sectionvalue' => $ip,
				  'file' => $l->{'file'} };
			&dovecot::create_section($conf, $pop3, $l);
			push(@{$l->{'members'}}, $pop3);
			push(@$conf, $pop3);
			}
		else {
			&dovecot::save_directive($pop3->{'members'}, $cname, $cvalue);
			&dovecot::save_directive($pop3->{'members'}, $kname, $kvalue);
			}
		&flush_file_lines($imap->{'file'}, undef, 1);
		}
	else {
		# Doesn't need one, either because SSL isn't enabled or the
		# domain doesn't have a private IP. So remove the local block.
		if ($l) {
			&dovecot::delete_section($conf, $l);
			@$conf = grep { $_ ne $l } @$conf;
			@$conf = grep { $_->{'sectionname'} ne $l->{'name'} ||
					$_->{'sectionvalue'} ne $l->{'value'} } @$conf;
			&flush_file_lines($l->{'file'});
			}
		else {
			# Nothing to add or remove
			$nochange = 1;
			}
		}
	}

if (!$d->{'virt'}) {
	# Domain has no IP, but Dovecot supports SNI in version 2
	my @loc = grep { $_->{'name'} eq 'local_name' &&
			 $_->{'section'} } @$conf;
	my @sslnames = &get_hostnames_from_cert($d);
	my %sslnames = map { $_, 1 } @sslnames;
	my @doms = ( $d, &get_domain_by("alias", $d->{'id'}) );
	my @myloc = grep { &hostname_under_domain(\@doms, $_->{'value'}) } @loc;
	my @dnames = map { ($_->{'dom'}, "*.".$_->{'dom'}) }
			 grep { !$_->{'deleting'} } @doms;
	my @delloc;
	if (!$enable) {
		# All existing local_name blocks are being removed
		@delloc = @myloc;
		}
	else {
		my $addfile = &dovecot::get_add_config_file();
		# May need to add or update
		foreach my $n (@dnames) {
			my ($l) = grep { $_->{'value'} eq $n } @loc;
			if ($l) {
				# Already exists, so update paths
				&dovecot::save_directive($l->{'members'}, $cname, $cvalue);
				&dovecot::save_directive($l->{'members'}, $kname, $kvalue);
				&flush_file_lines($l->{'file'}, undef, 1);
				}
			else {
				# Need to add
				my $l = { 'name' => 'local_name',
					  'value' => $n,
					  'enabled' => 1,
					  'section' => 1,
					  'members' => [
						{ 'name' => $cname,
						  'value' => $cvalue, },
						{ 'name' => $kname,
						  'value' => $kvalue, },
						],
					  'file' => $addfile };
				my ($plocal) = grep {
					$_->{'file'} eq $addfile &&
					&dovecot_local_name_cmp(
						$n, $_->{'value'}, $ver) < 0
					} @loc;
				&dovecot::create_section($conf, $l, undef,
							 $plocal);
				push(@$conf, $l);
				&flush_file_lines($l->{'file'}, undef, 1);
				@loc = grep { $_->{'name'} eq 'local_name' &&
					      $_->{'section'} } @$conf;
				}
			}
		# Find old entries to remove
		foreach my $l (@myloc) {
			my ($n) =  grep { $l->{'value'} eq $_ } @dnames;
			if (!$n) {
				push(@delloc, $l);
				}
			}
		}
	if (@delloc) {
		# Remove those to delete
		foreach my $l (@delloc) {
			&dovecot::delete_section($conf, $l);
			@$conf = grep { $_ ne $l } @$conf;
			@$conf = grep { $_->{'sectionname'} ne $l->{'name'} ||
					$_->{'sectionvalue'} ne $l->{'value'} } @$conf;
			&flush_file_lines($l->{'file'});
			}
		}
	}
&unlock_file($cfile);
&dovecot::apply_configuration() if (!$nochange);
return 1;
}

# get_dovecot_ssl_cert(&domain)
# Returns the path to the cert, key and CA cert, and optionally domain and IP
# in the Dovecot config for a domain, if any
sub get_dovecot_ssl_cert
{
my ($d) = @_;
return ( ) if (!&foreign_installed("dovecot"));
&foreign_require("dovecot");
my $ver = &dovecot::get_dovecot_version();
return ( ) if ($ver < 2);
my $conf = &dovecot::get_config();
my @rv = &get_dovecot_ssl_cert_name($d, $conf);
@rv = &get_dovecot_ssl_cert_ip($d, $conf) if (!@rv);
return @rv;
}

# get_dovecot_ssl_cert_ip(&domain, &conf)
# Lookup a domain's Dovecot cert by IP address
sub get_dovecot_ssl_cert_ip
{
my ($d, $conf) = @_;
my @loc = grep { $_->{'name'} eq 'local' &&
		 $_->{'section'} } @$conf;
my ($l, $l6, $imap, $imap6);
if ($d->{'ip'}) {
	# Check for an IPv4 specific block
	($l) = grep { $_->{'value'} eq $d->{'ip'} } @loc;
	($imap) = grep { $_->{'name'} eq 'protocol' &&
			 $_->{'value'} eq 'imap' &&
			 $_->{'enabled'} &&
			 $_->{'sectionname'} eq 'local' &&
			 $_->{'sectionvalue'} eq $d->{'ip'} } @$conf;
	}
if ($d->{'ip6'}) {
	# Also check if there's an IPv6 specific block
	($l6) = grep { $_->{'value'} eq "[".$d->{'ip6'}."]" } @loc;
	($imap6) = grep { $_->{'name'} eq 'protocol' &&
			  $_->{'value'} eq 'imap' &&
			  $_->{'enabled'} &&
			  $_->{'sectionname'} eq 'local' &&
			  $_->{'sectionvalue'} eq "[".$d->{'ip6'}."]"} @$conf;
	}
return ( ) if (!$l && !$l6);
return ( ) if (!$imap && !$imap6);

# Use whichever IP block exists
my $imap_block = $imap || $imap6;
my %mems = map { $_->{'name'}, $_->{'value'} }
	       @{$imap_block->{'members'}};
my @rv = ( $mems{'ssl_cert'} || $mems{'ssl_server_cert_file'},
           $mems{'ssl_key'} || $mems{'ssl_server_key_file'},
           $mems{'ssl_ca'},
           $imap ? $d->{'ip'} : undef,
           undef,	# No domain name for an IP-based certificate
           $imap6 ? $d->{'ip6'} : undef,
	 );
return () if (!$rv[0]);
foreach my $r (@rv) {
	$r =~ s/^<//;
	}
return @rv;
}

# get_dovecot_ssl_cert_name(&domain, &conf)
# Lookup a domain's Dovecot cert by domain name
sub get_dovecot_ssl_cert_name
{
my ($d, $conf) = @_;
my @loc = grep { $_->{'name'} eq 'local_name' &&
		 $_->{'section'} } @$conf;
my ($l) = grep { &hostname_under_domain($d, $_->{'value'}) } @loc;
return ( ) if (!$l);
my %mems = map { $_->{'name'}, $_->{'value'} } @{$l->{'members'}};
my @rv = ( $mems{'ssl_cert'} || $mems{'ssl_server_cert_file'},
	   $mems{'ssl_key'} || $mems{'ssl_server_key_file'},
	   $mems{'ssl_ca'},
	   undef,
	   $d->{'dom'} );
return () if (!$rv[0]);
foreach my $r (@rv) {
	$r =~ s/^<//;
	}
return @rv;
}

# postfix_supports_sni()
# Returns 1 if the installed version of Postfix supports name-based SSL certs
sub postfix_supports_sni
{
return 0 if ($mail_system != 0);
&foreign_require("postfix");
return &compare_versions($postfix::postfix_version, 3.4) >= 0;
}

# sync_postfix_ssl_cert(&domain, enable)
# Configure Postfix to use a domain's SSL cert for connections on its IP
sub sync_postfix_ssl_cert
{
my ($d, $enable) = @_;
my $tmpl = &get_template($d->{'template'});

# Check if Postfix is in use
return -1 if ($mail_system != 0);

my $changed = 0;
&foreign_require("postfix");
if ($d->{'virt'}) {
	# Setup per-IP cert in master.cf

	# Check if using SSL globally
	my $cfile = &postfix::get_real_value("smtpd_tls_cert_file");
	my $kfile = &postfix::get_real_value("smtpd_tls_key_file");
	my $cafile = &postfix::get_real_value("smtpd_tls_CAfile");
	return 0 if ($enable && (!$cfile || !$kfile) &&
		     !&domain_has_ssl_cert($d));

	# Find the existing master file entry
	&lock_file($postfix::config{'postfix_master'});
	my $master = &postfix::get_master_config();
	my $defip = &get_default_ip();

	# Work out which flags are needed
	my $chain = &domain_has_ssl_cert($d) ?
			&get_website_ssl_file($d, 'ca') : $cafile;
	my @flags = ( [ "smtpd_tls_cert_file",
			   &domain_has_ssl_cert($d) ?
				$d->{'ssl_cert'} : $cfile ],
			 [ "smtpd_tls_key_file",
			   &domain_has_ssl_cert($d) ?
				$d->{'ssl_key'} : $kfile ] );
	push(@flags, [ "smtpd_tls_CAfile", $chain ]) if ($chain);
	push(@flags, [ "smtpd_tls_security_level", "may" ]);
	push(@flags, [ "myhostname", $d->{'dom'} ]);

	foreach my $pfx ('smtp', 'submission', 'smtps', 'submissions', '465') {
		# Find the existing entry for the IP and for the default service
		my $already;
		my $already6;
		my $smtp;
		my @others;
		my $lsmtp;
		foreach my $m (@$master) {
			if ($d->{'ip'} &&
			    $m->{'name'} eq $d->{'ip'}.':'.$pfx &&
			    $m->{'enabled'} &&
			    $d->{'ip'} ne $defip) {
				# Entry for service for the IP
				$already = $m;
				}
			if ($d->{'ip6'} && $d->{'virt6'} &&
			    $m->{'name'} eq '['.$d->{'ip6'}.']:'.$pfx &&
			    $m->{'enabled'}) {
				# Entry for service for the IPv6
				$already6 = $m;
				}
			if (($m->{'name'} eq $pfx ||
			     $m->{'name'} eq $defip.':'.$pfx) &&
			    $m->{'type'} eq 'inet' && $m->{'enabled'}) {
				# Entry for default service
				$smtp = $m;
				}
			if ($m->{'name'} =~ /^([0-9\.]+):\Q$pfx\E$/ &&
			    $m->{'enabled'} && (!$d->{'ip'} || $1 ne $d->{'ip'}) &&
			    $1 ne $defip) {
				# Entry for some other domain
				if ($1 eq "127.0.0.1") {
					$lsmtp = $m;
					}
				else {
					push(@others, $m);
					}
				}
			}
		next if (!$smtp && $enable);

		if ($enable) {
			# Create or update the entry
			if (!$already) {
				# Create based on smtp inet entry
				$already = &create_ssl_postfix_service(
					$smtp, $d, $pfx, $d->{'ip'}, \@flags);
				&postfix::create_master($already);
				$changed = 1;

				# If the primary smtp entry isn't bound to an
				# IP, fix it to prevent IP clashes
				if ($smtp->{'name'} eq $pfx) {
					$smtp->{'name'} = $defip.':'.$pfx;
					&postfix::modify_master($smtp);

					# Also add an entry to listen on
					# 127.0.0.1
					if (!$lsmtp) {
						$lsmtp = { %$smtp };
						delete($lsmtp->{'line'});
						delete($lsmtp->{'uline'});
						$lsmtp->{'name'} =
							'127.0.0.1:'.$pfx;
						&postfix::create_master($lsmtp);
						}
					}
				}
			else {
				# Update cert file paths
				&update_ssl_postfix_service($already, \@flags);
				&postfix::modify_master($already);
				$changed = 1;
				}

			# Apply the same fixes for IPv6
			if (!$already6 && $d->{'virt6'}) {
				$already6 = &create_ssl_postfix_service(
					$smtp, $d, $pfx, "[".$d->{'ip6'}."]",
					\@flags);
				&postfix::create_master($already6);
				$changed = 1;
				}
			elsif ($already6 && $d->{'virt6'}) {
				# Update cert file paths
				&update_ssl_postfix_service($already6, \@flags);
				&postfix::modify_master($already6);
				$changed = 1;
				}
			elsif ($already6 && !$d->{'virt6'}) {
				# Remove special IPv6 entry
				&postfix::delete_master($already6);
				$changed = 1;
				}
			}
		else {
			# Remove the entry
			if ($already) {
				&postfix::delete_master($already);
				$changed = 1;
				}
			if ($already6) {
				&postfix::delete_master($already6);
				$changed = 1;
				}
			if (!@others && $smtp && $smtp->{'name'} ne $pfx) {
				# If the default service has an IP but this is
				# no longer needed, remove it
				$smtp->{'name'} = $pfx;
				&postfix::modify_master($smtp);
				$changed = 1;

				# Also remove 127.0.0.1 entry
				if ($lsmtp) {
					&postfix::delete_master($lsmtp);
					}
				}
			}
		}
	&unlock_file($postfix::config{'postfix_master'});
	}
elsif (&postfix_supports_sni()) {
	# Check if Postfix has an SNI map defined
	my $maphash = &postfix::get_current_value("tls_server_sni_maps");
	my $mapfile;
	if (!$maphash) {
		# No, so add it
		$mapfile = &postfix::guess_config_dir()."/sni_map";
		$maphash = &default_postfix_map_type().":".$mapfile;
		&postfix::set_current_value("tls_server_sni_maps", $maphash);
		&postfix::ensure_map("tls_server_sni_maps");
		$changed++;
		}
	else {
		($mapfile) = &postfix::get_maps_files($maphash);
		}

	# Is there an entry for this domain already?
	&lock_file($mapfile);
	my $map = &postfix::get_maps("tls_server_sni_maps");
	my @certs = ( $d->{'ssl_key'}, $d->{'ssl_cert'} );
	push(@certs, $d->{'ssl_chain'}) if ($d->{'ssl_chain'});
	my $certstr = join(",", @certs);
	my @doms = ( $d, &get_domain_by("alias", $d->{'id'}) );
	my @dnames = map { ($_->{'dom'}, ".".$_->{'dom'}) }
			 grep { !$_->{'deleting'} } @doms;
	my @mymaps = grep { &hostname_under_domain(\@doms,$_->{'name'}) } @$map;
	my @delmaps;
	if (!$enable) {
		# Deleting them all
		@delmaps = @mymaps;
		}
	else {
		# Add or update map entries for domain
		my $pdname = $d->{'dom'};
		$pdname =~ s/^[^\.]+\.//;
		foreach my $dname (@dnames) {
			my ($r) = grep { $_->{'name'} eq $dname } @$map;
			my ($pdr) = grep { $_->{'name'} eq $pdname } @$map;
			if (!$r) {
				# Need to add
				&postfix::create_mapping(
				    "tls_server_sni_maps",
				    { 'name' => $dname, 'value' => $certstr },
				    undef, undef, $pdr);
				}
			else {
				# Update existing certs
				$r->{'value'} = $certstr;
				&postfix::modify_mapping(
				    "tls_server_sni_maps", $r, $r);
				}
			}
		# Identify those no longer needed
		foreach my $r (@mymaps) {
			my ($n) =  grep { $r->{'name'} eq $_ } @dnames;
			if (!$n) {
				push(@delmaps, $r);
				}
			}
		}
	if (@delmaps) {
		# Remove un-needed map entries
		foreach my $r (reverse(@delmaps)) {
			&postfix::delete_mapping("tls_server_sni_maps", $r);
			}
		}
	&unlock_file($mapfile);
	&postfix::regenerate_sni_table();
	}
else {
	# Cannot use per-domain or per-IP cert
	return 0;
	}
&register_post_action(\&restart_mail_server);
return 1;
}

# create_ssl_postfix_service(&base-smtp, &domain, name, ip, &flags)
# Returns a new Postfix master.cf entry setup for SSL
sub create_ssl_postfix_service
{
my ($smtp, $d, $pfx, $ip, $flags) = @_;
my $svc = { %$smtp };
delete($svc->{'line'});
delete($svc->{'uline'});
$svc->{'name'} = $ip.':'.$pfx;
foreach my $f (@$flags) {
	$svc->{'command'} .= " -o ".$f->[0]."=".$f->[1];
	}
$svc->{'command'} =~ s/-o smtpd_(client|helo|sender)_restrictions=\$mua_client_restrictions\s+//g;
return $svc;
}

# update_ssl_postfix_service(&service, &flags)
# Update existing flags to match new values, or add if missing
sub update_ssl_postfix_service
{
my ($svc, $flags) = @_;
foreach my $f (@$flags) {
	if ($svc->{'command'} !~ s/-o\s+\Q$f->[0]\E=(\S+)/-o $f->[0]=$f->[1]/) {
		$svc->{'command'} .= " -o ".$f->[0]."=".$f->[1];
		}
	}
}

# get_postfix_ssl_cert(&domain)
# Returns the path to the cert, key and CA cert in the Postfix config for
# a domain, if any
sub get_postfix_ssl_cert
{
my ($d) = @_;
return ( ) if ($mail_system != 0);
&foreign_require("postfix");

# First check for a per-domain cert
if (&postfix::get_current_value("tls_server_sni_maps")) {
	my $map = &postfix::get_maps("tls_server_sni_maps");
	my ($already) = grep { &hostname_under_domain($d, $_->{'name'}) } @$map;
	if ($already) {
		# Found it!
		my @certs = split(/,/, $already->{'value'});
		return ($certs[1], $certs[0], $certs[2], undef, $d->{'dom'});
		}
	}

# Fall back to checking for a per-IP cert
my $master = &postfix::get_master_config();
my @rv;
foreach my $m (@$master) {
	if ($d->{'ip'} && $m->{'name'} eq $d->{'ip'}.':smtp' && $m->{'enabled'} &&
	    $m->{'command'} =~ /smtpd_tls_cert_file=(\S+)/) {
		@rv = ( $1 );
		push(@rv, $m->{'command'} =~ /smtpd_tls_key_file=(\S+)/
				? $1 : undef);
		push(@rv, $m->{'command'} =~ /smtpd_tls_CAfile=(\S+)/
				? $1 : undef);
		push(@rv, $d->{'ip'});
		push(@rv, undef);
		}
	}
foreach my $m (@$master) {
	if ($d->{'ip6'} && $m->{'name'} eq '['.$d->{'ip6'}.']:smtp' && $m->{'enabled'} &&
	    @rv && $m->{'command'} =~ /smtpd_tls_cert_file=(\S+)/ &&
	    $1 eq $rv[0]) {
		push(@rv, $d->{'ip6'});
		}
	}
return @rv;
}

# get_hostnames_for_ssl(&domain)
# Returns a list of names that should be used in an SSL cert, based on their
# IP address and whether Apache is configured to accept them.
sub get_hostnames_for_ssl
{
my ($d) = @_;
my @rv = ( $d->{'dom'} );
my $defvirt;
my $p = &domain_has_website($d);
return @rv if (!$p);
if ($p eq "web") {
	$defvirt = &get_apache_virtual($d->{'dom'}, $d->{'web_port'});
	}
else {
	$defvirt = &plugin_call($p, "feature_get_domain_web_config",
				$d->{'dom'}, $d->{'web_port'});
	}
return @rv if (!$defvirt);
my @webmail;
if ($p eq "web") {
	@webmail = &get_webmail_redirect_directives($d);
	}
my @recs;
if ($d->{'dns'}) {
	@recs = &get_domain_dns_records($d);
	}
foreach my $full ("www.".$d->{'dom'},
		  ($d->{'mail'} ? ("mail.".$d->{'dom'}) : ()),
		  "admin.".$d->{'dom'},
		  "webmail.".$d->{'dom'},
		  "mta-sts.".$d->{'dom'},
		  &get_autoconfig_hostname($d)) {
	# Is the webserver configured to serve this hostname?
	my $virt;
	if ($p eq "web") {
		$virt = &get_apache_virtual($full, $d->{'web_port'});
		}
	else {
		$virt = &plugin_call($p, "feature_get_domain_web_config",
				     $full, $d->{'web_port'});
		}
	next if (!$virt || $virt ne $defvirt);

	# If Apache, is there an unconditional rewrite for this hostname?
	my $found;
	foreach my $wm (@webmail) {
		if ($wm->[0] eq $full && $wm->[1] eq '^(.*)') {
			$found = 1;
			}
		}
	next if ($found);

	# Is there a DNS entry for this hostname?
	if ($d->{'dns'}) {
		my ($r) = grep { $_->{'name'} eq $full."." } @recs;
		if ($r) {
			push(@rv, $full);
			}
		}
	if (&to_ipaddress($full)) {
		push(@rv, $full);
		}
	}
if (!$d->{'alias'}) {
	# Add aliases of this domain that have SSL enabled
	foreach my $alias (&get_domain_by("alias", $d->{'id'})) {
		if (&domain_has_website($alias) && !$alias->{'disabled'} &&
		    !$alias->{'deleting'}) {
			push(@rv, &get_hostnames_for_ssl($alias));
			}
		}
	}
return &unique(@rv);
}

# get_hostnames_from_cert(&domain)
# Returns a list of hostnames that the domain's cert is valid for
sub get_hostnames_from_cert
{
my $info = &cert_info($d);
return () if (!$info);
my @rv = ( $info->{'cn'} );
push(@rv, @{$info->{'alt'}}) if ($info->{'alt'});
return @rv;
}

# is_acme_cert(&info|&domain)
# Returns 1 if a cert info looks like it comes from supported ACME provider
sub is_acme_cert
{
my ($info) = @_;
if ($info->{'dom'} && $info->{'id'}) {
	# Looks like a virtual server
	$info = &cert_info($info);
	}

# Check all issuer names (if the list sub exists)
my @patterns = ("Let's\\s+Encrypt");
if (defined &list_known_acme_providers) {
	push(@patterns, map { quotemeta($_->{'issuer'}) }
		&list_known_acme_providers());
	}

# Check the issuer name
my $re = join('|', @patterns);
return $info && ($info->{'issuer_cn'} =~ /$re/i ||
		 $info->{'issuer_o'}  =~ /$re/i);
}

# apply_letsencrypt_cert_renewals()
# Check all domains that need a new Let's Encrypt cert
sub apply_letsencrypt_cert_renewals
{
my $le_max_renewals = 300.0;
my $le_max_time = 3*60*60;	# 3 hours
my $le_fail_slowdown = 5*24*60*60;	# After failing for this long ..
my $le_fail_interval = 24*60*60;	# .. retry only once a day
my $last_renew_time = $config{'last_letsencrypt_mass_renewal'};
my $now = time();

my $done = 0;
foreach my $d (&list_domains()) {
	# Does the domain have SSL enabled and a renewal policy?
	next if (!&domain_has_ssl_cert($d) || !$d->{'letsencrypt_renew'});

	# Does the domain have it's own SSL cert?
	next if ($d->{'ssl_same'});

	# Is the domain enabled?
	next if ($d->{'disabled'});

	# Get the cert and date
	my $info = &cert_info($d);
	next if (!$info);
	my $expiry = &parse_notafter_date($info->{'notafter'});

	# Is the current cert even from an SSL provider?
	next if (!&is_acme_cert($info));

	# Figure out when the cert was last renewed. This is the max of the
	# date in the cert and the time recorded in Virtualmin
	my $ltime = &parse_notafter_date($info->{'notbefore'});
	$ltime = $d->{'letsencrypt_last'}
		if ($d->{'letsencrypt_last'} > $ltime);

	# If an attempt was made in the last hour, skip for now to prevent
	# hammering the Let's Encrypt service
	next if (time() - $d->{'letsencrypt_last'} < 60*60);

	# If renewals have been failing continuously for several days, slow
	# down to one attempt per day, so that the ACME provider doesn't
	# pause issuance for the domain due to too many failed requests
	if ($d->{'letsencrypt_first_failure'} &&
	    $d->{'letsencrypt_first_failure'} >
	      ($d->{'letsencrypt_last_success'} || 0) &&
	    time() - $d->{'letsencrypt_first_failure'} > $le_fail_slowdown) {
		next if (time() - $d->{'letsencrypt_last_failure'} <
			 $le_fail_interval);
		}

	# Is it time? Either the user-chosen number of months has passed, or
	# the cert is within 21 days of expiry
	my $tmpl = &get_template($d->{'template'});
	my $before = $tmpl->{'ssl_renew_letsencrypt'} || 21;
	my $day = 24 * 60 * 60;
	my $age = time() - $ltime;
	my $rf = rand() * 3600;
	my $renew = $expiry && $expiry - time() < $before * $day + $rf;
	next if (!$renew);

	# Don't even attempt now if the lock is being held
	next if (&test_lock($ssl_letsencrypt_lock));

	# Don't exceed the global let's encrypt rate limit
	if ($last_renew_time) {
		my $diff = $now - $last_renew_time;
		if ($done > $le_max_renewals / $le_max_time * $diff / 2) {
			# Done too much this cycle (more than half of the limit)
			last;
			}
		}

	# Find the SSL provider name
	my $pname;
	if (defined(&list_acme_providers)) {
		my ($acme) = grep { $_->{'id'} eq $d->{'letsencrypt_id'} }
                                  &list_acme_providers();
		if ($acme->{'type'}) {
			my ($prov) = grep { $_->{'id'} eq $acme->{'type'} }
				          &list_known_acme_providers();
			$pname = $prov->{'desc'};
			}
		else {
			$pname = $acme->{'desc'};
			}
		}
	else {
		$pname = $text{'acme_letsencrypt'};
		}

	# Time to attempt the renewal
	$done++;
	my ($ok, $err, $dnames) = &renew_letsencrypt_cert($d);
	my ($subject, $body);
	&lock_domain($d);
	$d = &get_domain($d->{'id'}, undef, 1);
	if (!$ok) {
		# Failed! Tell the user
		$subject = $text{'letsencrypt_sfaileda'};
		$body = &text('letsencrypt_bfaileda',
			      join(", ", @$dnames), $err, $pname);
		$d->{'letsencrypt_last'} = time();
		$d->{'letsencrypt_last_failure'} = time();
		$d->{'letsencrypt_first_failure'} ||= time();
		$err =~ s/\r?\n/\t/g;
		$d->{'letsencrypt_last_err'} = $err;
		}
	else {
		# Tell the user it worked
		delete($d->{'letsencrypt_last_err'});
		delete($d->{'letsencrypt_first_failure'});
		$subject = $text{'letsencrypt_sdonea'};
		$body = &text('letsencrypt_bdonea',
			      join(", ", @$dnames), $pname);
		}
	&save_domain($d);

	# Send email
	my $from = &get_global_from_address($d);
	if ($d->{'letsencrypt_email'} == 0 ||
	    $d->{'letsencrypt_email'} == 1 && !$ok) {
		&send_notify_email(
			$from, [$d], $d, $subject, $body, undef,
			undef, undef, undef, undef, undef,
			$d->{'letsencrypt_email_master'} ?
				$gconfig{'webmin_email_to'} : undef);
		}
	&unlock_domain($d);
	}

$config{'last_letsencrypt_mass_renewal'} = $now;
&lock_file($module_config_file);
&save_module_config();
&unlock_file($module_config_file);
}

# renew_letsencrypt_cert(&domain)
# Re-request the Let's Encrypt cert for a domain. 
sub renew_letsencrypt_cert
{
my ($d) = @_;

# Work out the hostnames to request
my @dnames;
if ($d->{'letsencrypt_dname'}) {
	@dnames = split(/\s+/, $d->{'letsencrypt_dname'});
	}
else {
	@dnames = &get_hostnames_for_ssl($d);
	}
push(@dnames, "*.".$d->{'dom'}) if ($d->{'letsencrypt_dwild'});
my $fdnames = &filter_ssl_wildcards(\@dnames);
@dnames = @$fdnames;
my $has_ips = &letsencrypt_dnames_have_ips(\@dnames);
my $nerr = &validate_letsencrypt_ip_cert_names(\@dnames);
return (0, $nerr, \@dnames) if ($nerr);
my $rerr = &validate_letsencrypt_ip_cert_renewal($d, \@dnames, 1);
return (0, $rerr, \@dnames) if ($rerr);
if (defined($d->{'letsencrypt_nodnscheck'}) &&
    !$d->{'letsencrypt_nodnscheck'}) {
	my @badnames;
	my $fok = &filter_external_dns(\@dnames, \@badnames);
	if (!@dnames) {
		return (0, "None of the hostnames could be resolved", \@badnames);
		}
	}

# Run the before command
&set_domain_envs($d, "SSL_DOMAIN");
my $merr = &making_changes();
&reset_domain_envs($d);
if ($merr) {
	# Pre-command failed
	return (0, $merr, \@dnames);
	}

# Time to do it!
my $phd = &public_html_dir($d);
&foreign_require("webmin");
my $before = &before_letsencrypt_website($d);
my $acme;
if ($d->{'letsencrypt_id'} && defined(&list_acme_providers)) {
	($acme) = grep { $_->{'id'} eq $d->{'letsencrypt_id'} }
		       &list_acme_providers();
	}
my ($ok, $cert, $key, $chain) =
	&request_domain_letsencrypt_cert($d, \@dnames, 0,
	    $d->{'letsencrypt_size'}, undef, $d->{'letsencrypt_ctype'},
	    $acme, $d->{'letsencrypt_subset'});
&after_letsencrypt_website($d, $before);

my ($subject, $body);
if (!$ok) {
	# Failed! Tell the user
	return (0, $cert, \@dnames);
	}

# Figure out which services (webmin, postfix, etc) were using the old cert
my @beforecerts = &get_all_domain_service_ssl_certs($d);

# Copy into place
&obtain_lock_ssl($d);
&install_letsencrypt_cert($d, $cert, $key, $chain);
&lock_domain($d);
$d->{'letsencrypt_last'} = time();
$d->{'letsencrypt_last_success'} = time();
$d->{'letsencrypt_last_id'} = $d->{'letsencrypt_id'};
delete($d->{'letsencrypt_last_err'});
delete($d->{'letsencrypt_first_failure'});
&save_domain($d);
&unlock_domain($d);
&release_lock_ssl($d);

# Update services that were using the old cert, both globally and per-domain
&update_all_domain_service_ssl_certs($d, \@beforecerts,
				     $has_ips ? 1 : undef);

# Update DANE DNS records now that all services point at the new cert
&sync_domain_tlsa_records($d);
foreach my $od (&get_domain_by("ssl_same", $d->{'id'})) {
	&sync_domain_tlsa_records($od);
	}

# Call the post command
&set_domain_envs($d, "SSL_DOMAIN");
&made_changes();
&reset_domain_envs($d);

return (1, undef, \@dnames);
}

# install_letsencrypt_cert(&domain, certfile, keyfile, chainfile)
# Update the current cert and key for a domain
sub install_letsencrypt_cert
{
my ($d, $cert, $key, $chain) = @_;

# Copy and save the cert
$d->{'ssl_cert'} ||= &default_certificate_file($d, 'cert');
my $cert_text = &read_file_contents($cert);
&lock_file($d->{'ssl_cert'});
&create_ssl_certificate_directories($d);
&write_ssl_file_contents($d, $d->{'ssl_cert'}, $cert_text);
&unlock_file($d->{'ssl_cert'});
&save_website_ssl_file($d, "cert", $d->{'ssl_cert'});

# And the key
$d->{'ssl_key'} ||= &default_certificate_file($d, 'key');
my $key_text = &read_file_contents($key);
&lock_file($d->{'ssl_key'});
&write_ssl_file_contents($d, $d->{'ssl_key'}, $key_text);
&unlock_file($d->{'ssl_key'});
&save_website_ssl_file($d, "key", $d->{'ssl_key'});

# Let's encrypt certs have no passphrase
$d->{'ssl_pass'} = undef;
&save_domain_passphrase($d);

# And the chained file
if ($chain) {
	$chainfile = $d->{'ssl_chain'} ||
	    &default_certificate_file($d, 'ca');
	$chain_text = &read_file_contents($chain);
	&lock_file($chainfile);
	&write_ssl_file_contents($d, $chainfile, $chain_text);
	&unlock_file($chainfile);
	$err = &save_website_ssl_file($d, 'ca', $chainfile);
	$d->{'ssl_chain'} = $chainfile;
	}

# Create the combined cert file
&sync_combined_ssl_cert($d);

# If the domain has DNS, setup a CAA record
&update_caa_record($d);
}

# get_caa_for_cert(&info)
# Returns the CAA domain that matches the cert's issuer, falling back to
# letsencrypt.org if no match
sub get_caa_for_cert
{
my ($info) = @_;
return 'letsencrypt.org' unless defined &list_known_acme_providers;
foreach my $p (&list_known_acme_providers()) {
	return $p->{'caa'}
	  if ( $info->{'issuer_cn'} =~ /\Q$p->{'issuer'}\E/i ||
	       $info->{'issuer_o'}  =~ /\Q$p->{'issuer'}\E/i );
	}
return 'letsencrypt.org';
}

# update_caa_record(&domain, [force-letsencrypt])
# Update the CAA record for the active ACME provider if needed
sub update_caa_record
{
my ($d, $letsencrypt_cert) = @_;
&require_bind();
return undef if (!$d->{'dns'});
return undef if (!$d->{'dns_cloud'} &&
		 &compare_version_numbers($bind8::bind_version, "9.9.6") < 0);
my ($recs, $file) = &get_domain_dns_records_and_file($d);
my @caa = grep { $_->{'type'} eq 'CAA' } @$recs;
# At this stage the cert is always self-signed,
# so we need to force it for Let's Encrypt
my $lets = $letsencrypt_cert;
my $info = &cert_info($d) || {};
my $caa_value = &get_caa_for_cert($info);
if (!$lets) {
	$lets = &is_acme_cert($info) ? 1 : 0;
	}
# Need delay for DNS propagation
if (!@caa && $lets) {
	# Need to add the record
	&pre_records_change($d);
	my $caa = { 'name'   => '@',
		    'type'   => 'CAA',
		    'values' => [ "0", "issue", $caa_value ] };
	&create_dns_record($recs, $file, $caa);
	&post_records_change($d, $recs, $file);
	&reload_bind_records($d);
	}
elsif (@caa == 1 && $caa[0]->{'values'}->[1] =~ /^issue(wild)?$/) {
	my $current_val = $caa[0]->{'values'}->[2];
	if (!$lets) {
		# Need to remove the record
		&pre_records_change($d);
		&delete_dns_record($recs, $file, $caa[0]);
		}
	elsif ($current_val ne $caa_value) {
		# Provider changed, need to update
		&pre_records_change($d);
		my $oldwild = $caa[0]->{'values'}->[1];
		&delete_dns_record($recs, $file, $caa[0]);
		&create_dns_record($recs, $file,
			{ 'name'   => '@',
			  'type'   => 'CAA',
			  'values' => [ 0, $oldwild, $caa_value ] } );
		}
	else {
		return;
		}
	
	# Apply changes
	&post_records_change($d, $recs, $file);
	&reload_bind_records($d);
	}
}

# sync_combined_ssl_cert(&domain)
# If a domain has a regular cert and a CA cert, combine them into one file
sub sync_combined_ssl_cert
{
my ($d) = @_;
if ($d->{'ssl_same'}) {
	# Assume parent has the combined files
	my $sslclash = &get_domain($d->{'ssl_same'});
	&sync_combined_ssl_cert($sslclash);
	$d->{'ssl_combined'} = $sslclash->{'ssl_combined'};
	$d->{'ssl_everything'} = $sslclash->{'ssl_everything'};
	return;
	}

# Create file of all the certs
my $combfile = $d->{'ssl_combined'} ||
       &default_certificate_file($d, 'combined');
&lock_file($combfile);
&create_ssl_certificate_directories($d);
my $comb = &read_file_contents($d->{'ssl_cert'})."\n";
if (-r $d->{'ssl_chain'}) {
	$comb .= &read_file_contents($d->{'ssl_chain'})."\n";
	}
&write_ssl_file_contents($d, $combfile, $comb);
&unlock_file($combfile);
$d->{'ssl_combined'} = $combfile;

# Create file of all the certs, and the key
my $everyfile = $d->{'ssl_everything'} ||
       &default_certificate_file($d, 'everything');
&lock_file($everyfile);
my $every = &read_file_contents($d->{'ssl_key'})."\n".
	    &read_file_contents($d->{'ssl_cert'})."\n";
if (-r $d->{'ssl_chain'}) {
	$every .= &read_file_contents($d->{'ssl_chain'})."\n";
	}
&write_ssl_file_contents($d, $everyfile, $every);
&unlock_file($everyfile);
$d->{'ssl_everything'} = $everyfile;
}

# get_openssl_version()
# Returns the version of the installed OpenSSL command
sub get_openssl_version
{
my $out = &backquote_command("openssl version 2>/dev/null");
if ($out =~ /OpenSSL\s+(\d\.\d\.\d)/) {
	return $1;
	}
return 0;
}

# before_letsencrypt_website(&domain)
# If there is any proxy setup that would block /.well-known, add a negative
# path to ensure direct access
sub before_letsencrypt_website
{
my ($d) = @_;
my $rv = { };
&push_all_print();
&set_all_null_print();
&setup_noproxy_path($d, { 'uses' => [ 'proxy' ] }, undef,
		    { 'path' => '/.well-known' });
if (&has_web_redirects($d)) {
	# Remove redirects that may block let's encrypt
	my @redirs;
	foreach my $r (&list_redirects($d)) {
		if ($r->{'path'} eq '/' && $r->{'http'}) {
			# Possible problem redirect
			&delete_redirect($d, $r);
			push(@redirs, { %$r });
			}
		}
	if (@redirs) {
		&run_post_actions();
		$rv->{'redirs'} = \@redirs;
		}
	}
&pop_all_print();
return $rv;
}

# after_letsencrypt_website(&domain, &before-rv)
# Undoes changes made by before_letsencrypt_website
sub after_letsencrypt_website
{
my ($d, $before) = @_;
&push_all_print();
&set_all_null_print();
if ($before->{'redirs'}) {
	foreach my $r (@{$before->{'redirs'}}) {
		&create_redirect($d, $r);
		}
	&run_post_actions();
	}
&pop_all_print();
}

# filter_ssl_wildcards(&hostnames)
# Returns an array ref of hostnames for an SSL cert request, minus any that
# are redundant due to wildcards
sub filter_ssl_wildcards
{
my ($dnames) = @_;
my @rv;
my %wild;
foreach my $h (@$dnames) {
	if ($h =~ /^\*\.(.*)$/) {
		$wild{$1} = 1;
		}
	}
foreach my $h (@$dnames) {
	next if ($h =~ /^([^\.\*]+)\.(.*)$/ && $wild{$2});
	push(@rv, $h);
	}
return \@rv;
}

# letsencrypt_is_ip_cert_identifier(identifier)
# Returns 1 if some Let's Encrypt certificate identifier is an IP address.
sub letsencrypt_is_ip_cert_identifier
{
my ($id) = @_;
return 0 if (!$id);
&foreign_require("webmin");
if (defined(&webmin::letsencrypt_is_ip_cert_identifier)) {
	return &webmin::letsencrypt_is_ip_cert_identifier($id);
	}
return &ssl_cert_identifier_is_ip($id);
}

# letsencrypt_invalid_ip_identifier_error(identifier)
# Returns an error if an identifier looks like an IP address but is invalid.
sub letsencrypt_invalid_ip_identifier_error
{
my ($id) = @_;
return undef if (!$id || &letsencrypt_is_ip_cert_identifier($id));
return &text('letsencrypt_eipbad', $id)
	if ($id =~ /:/ || $id =~ /^[0-9]+(?:\.[0-9]+){3}$/);
return undef;
}

# letsencrypt_dnames_have_ips(&dnames)
# Returns 1 if any Let's Encrypt certificate identifier is an IP address.
sub letsencrypt_dnames_have_ips
{
my ($dnames) = @_;
foreach my $dname (@$dnames) {
	return 1 if (&letsencrypt_is_ip_cert_identifier($dname));
	}
return 0;
}

# validate_letsencrypt_ip_cert_names(&dnames)
# Returns an error if IP identifiers are combined with incompatible names.
sub validate_letsencrypt_ip_cert_names
{
my ($dnames) = @_;
return undef if (!&letsencrypt_dnames_have_ips($dnames));
# IP identifiers require web validation, while wildcards require DNS validation.
# Virtualmin does not mix those validation modes in one certificate request.
my @wilds = grep { /^\*\./ } @$dnames;
return @wilds ? $text{'letsencrypt_eipwild'} : undef;
}

# letsencrypt_ip_cert_renewal_max_days()
# Returns the maximum renewal threshold for short-lived IP certificates.
sub letsencrypt_ip_cert_renewal_max_days
{
return 5;
}

# letsencrypt_renewal_before_days(&domain)
# Returns the template renewal threshold in days before expiry.
sub letsencrypt_renewal_before_days
{
my ($d) = @_;
my $tmpl = &get_template($d->{'template'});
return $tmpl->{'ssl_renew_letsencrypt'} || 21;
}

# validate_letsencrypt_ip_cert_renewal(&domain, &dnames, renew)
# Returns an error if automatic renewal is unsafe for IP certificates.
sub validate_letsencrypt_ip_cert_renewal
{
my ($d, $dnames, $renew) = @_;
return undef if (!$renew || !&letsencrypt_dnames_have_ips($dnames));
my $days = &letsencrypt_renewal_before_days($d);
my $max = &letsencrypt_ip_cert_renewal_max_days();
return &text('letsencrypt_eiprenew', $days, $max) if ($days > $max);
return undef;
}

# validate_letsencrypt_ip_cert_support()
# Returns an error if Certbot cannot request IP identifier certificates
# in Virtualmin's webroot validation mode.
sub validate_letsencrypt_ip_cert_support
{
&foreign_require("webmin");
return $text{'letsencrypt_eipnowebmin'}
	if (!defined(&webmin::letsencrypt_is_ip_cert_identifier) ||
	    !defined(&webmin::get_certbot_major_version));
return $text{'letsencrypt_eipnocertbot'}
	if (!$webmin::letsencrypt_cmd);
my $cmd_ver = &webmin::get_certbot_major_version($webmin::letsencrypt_cmd);
my $min_ver = "5.4";
return &text('letsencrypt_eipcertbotver', $min_ver)
	if (!$cmd_ver || &compare_version_numbers($cmd_ver, $min_ver) < 0);
return undef;
}

# validate_letsencrypt_ip_website(&domain, &dnames)
# Returns an error if IP identifiers cannot validate against this website.
sub validate_letsencrypt_ip_website
{
my ($d, $dnames) = @_;
my @ips = grep { &letsencrypt_is_ip_cert_identifier($_) } @$dnames;
return undef if (!@ips);
my $err = &validate_letsencrypt_ip_cert_support();
return $err if ($err);
return $text{'letsencrypt_eipnoweb'} if (!&domain_has_website($d));
foreach my $ip (@ips) {
	my $v6 = &check_ip6address($ip);
	my $certip = $v6 ? ($d->{'dns_ip6'} || $d->{'ip6'})
			 : ($d->{'dns_ip'} || $d->{'ip'});
	my $virt = $v6 ? $d->{'virt6'} : $d->{'virt'};
	if (!$certip ||
	    &normalize_ssl_cert_identifier($ip) ne
	    &normalize_ssl_cert_identifier($certip)) {
		return &text('letsencrypt_eipnotdom', $ip);
		}
	next if ($virt);
	# On a shared IP, ACME webroot validation for the IP lands on the
	# default website, so only that virtual server can safely request it.
	next if (&is_default_website($d));
	my $defd = &find_default_website($d);
	return &text('letsencrypt_eipnotdefault', $ip,
		     $defd ? $defd->{'dom'} : $text{'no'});
	}
return undef;
}

# request_domain_letsencrypt_cert(&domain, &dnames, [staging], [size], [mode],
# 				  [key-type], [&acme], [allow-subset],
# 				  [new-domain])
# Attempts to request a Let's Encrypt cert for a domain, trying both web and
# DNS modes if possible. The key type must be one of 'rsa' or 'ecdsa'
sub request_domain_letsencrypt_cert
{
my ($d, $dnames, $staging, $size, $mode, $ctype, $acme, $subset, $newdom) = @_;
my $tmpl = &get_template($d->{'template'});
my ($server, $keytype, $hmac);
if ($acme) {
	defined(&list_acme_providers) ||
	    return (0, "Custom ACME providers are only supported in Virtualmin Pro");
	$keytype = $acme->{'key'};
	$hmac = $acme->{'hmac'};
	if ($acme->{'type'}) {
		my ($prov) = grep { $_->{'id'} eq $acme->{'type'} }
				  &list_known_acme_providers();
		$prov || return (0, "ACME provider $acme->{'type'} does not exist");
		$server = ($staging ? $prov->{'staging_url'} : undef) ||
			  $prov->{'url'};
		}
	else {
		$server = $acme->{'url'};
		}
	}
my ($ok, $cert, $key, $chain, @errs);
$dnames = &filter_ssl_wildcards($dnames);
$size ||= $tmpl->{'ssl_key_size'};
&foreign_require("webmin");
my $phd = &public_html_dir($d);
my $actype = $ctype =~ /^ec/ ? "ecdsa" : "rsa";
my $dcinfo = &cert_info($d);
my $dclets = &is_acme_cert($d);
my $dcalgo = $dcinfo->{'algo'};
my $dctype = $dcalgo =~ /^ec/ ? "ecdsa" : "rsa";
my $actype_reuse = $actype eq $dctype &&
		   $size == $dcinfo->{'size'} ? 1 : 0;
$actype_reuse = -1 if (!$dcalgo || !$dclets);
my @wilds = grep { /^\*\./ } @$dnames;
my $has_ips = &letsencrypt_dnames_have_ips($dnames);
my $nerr = &validate_letsencrypt_ip_cert_names($dnames);
return (0, $nerr) if ($nerr);
my $iperr = &validate_letsencrypt_ip_website($d, $dnames);
return (0, $iperr) if ($iperr);
&lock_file($ssl_letsencrypt_lock);
&disable_quotas($d);
my $acme_email = $acme ? $acme->{'email'} : undef;
$acme_email ||= &get_global_from_address();
# Request now
@errs = ();
if (&domain_has_website($d) && !@wilds && (!$mode || $mode eq "web")) {
	# Wait for DNS propagation before initial web validation
	if ($newdom) {
		my %webmin_mod_config = &foreign_config("webmin");
		sleep($webmin_mod_config{'letsencrypt_dns_wait'} // 10);
		}
	# Try using website first
	($ok, $cert, $key, $chain) = &webmin::request_letsencrypt_cert(
		$dnames, $phd, $d->{'emailto'}, $size, "web", $staging,
		$acme_email, $actype, $actype_reuse,
		$server, $keytype, $hmac, $subset);
	push(@errs, &text('letsencrypt_eweb', $cert)) if (!$ok);
	}
# DNS validation cannot prove control of IP identifiers, so never fall
# back to it once the request contains any IP address.
if (!$ok && !$has_ips && $d->{'dns'} && (!$mode || $mode eq "dns")) {
	# Fall back to DNS
	($ok, $cert, $key, $chain) = &webmin::request_letsencrypt_cert(
		$dnames, undef, $d->{'emailto'}, $size, "dns", $staging,
		$acme_email, $actype, $actype_reuse,
		$server, $keytype, $hmac, $subset);
	push(@errs, &text('letsencrypt_edns', $cert)) if (!$ok);
	&clear_domain_dns_records_and_file($d);
	}
elsif (!$ok) {
	if (!$cert) {
		$cert = $has_ips ? $text{'letsencrypt_eipnoweb'} :
			"Domain has no website, ".
			"and DNS-based validation is not possible";
		push(@errs, $cert);
		}
	}
&enable_quotas($d);
&unlock_file($ssl_letsencrypt_lock);
# Return results
if (!$ok) {
	return ($ok, join("&nbsp;&nbsp;&nbsp;", @errs), $key, $chain);
	}
else {
	return ($ok, $cert, $key, $chain);
	}
}

# validate_letsencrypt_config(&domain, [&features])
# Returns a list of validation errors that might prevent Let's Encrypt
sub validate_letsencrypt_config
{
my ($d, $feats) = @_;
my @rv;
$feats ||= ["web", "dns"];
foreach my $f (@$feats) {
	if ($d->{$f} && $config{$f}) {
		my $vfunc = "validate_$f";
		my $err = $f eq "dns" ? &$vfunc($d, undef, 1)
				      : &$vfunc($d);
		if ($err) {
			push(@rv, { 'desc' => $text{'feature_'.$f},
				    'error' => $err });
			}
		else {
			# A valid method exists
			return ();
			}
		}
	}
return @rv;
}

# letsencrypt_supports_ec()
# Returns 1 if Let's Encrypt client supports EC certificates
sub letsencrypt_supports_ec
{
&foreign_require("webmin");
return 0 if (&webmin::check_letsencrypt());	# Not installed
return 0 if (!$webmin::letsencrypt_cmd);	# Missing native client
my $ver = &webmin::get_certbot_major_version($webmin::letsencrypt_cmd);
return &compare_versions($ver, 2.0) >= 0;
}

# ssl_cert_identifier_is_ip(identifier)
# Returns 1 if a certificate identifier is an IPv4 or IPv6 address.
sub ssl_cert_identifier_is_ip
{
my ($id) = @_;
return 1 if (&check_ipaddress($id));
return $id !~ /\// && $id =~ /:/ && &check_ip6address($id);
}

# ssl_cert_packed_ip_address(packed-ip)
# Converts a subjectAltName iPAddress value to text.
sub ssl_cert_packed_ip_address
{
my ($val) = @_;
return join(".", unpack("C4", $val)) if (length($val) == 4);
if (length($val) == 16) {
	eval "use Socket ();";
	my $ip = !$@ ? eval { Socket::inet_ntop(Socket::AF_INET6(), $val) }
		     : undef;
	return $ip if ($ip);
	return join(":", map { sprintf("%x", $_) } unpack("n8", $val));
	}
return $val;
}

# normalize_ssl_cert_identifier(identifier)
# Normalizes hostnames and IP addresses for certificate name comparison.
sub normalize_ssl_cert_identifier
{
my ($id) = @_;
return "" if (!defined($id));
if (&check_ipaddress($id)) {
	return join(".", map { int($_) } split(/\./, $id));
	}
if (&check_ip6address($id)) {
	eval "use Socket ();";
	if (!$@) {
		my $packed = eval { Socket::inet_pton(Socket::AF_INET6(), $id) };
		my $ip = $packed ?
			eval { Socket::inet_ntop(Socket::AF_INET6(), $packed) }
			: undef;
		return lc($ip) if ($ip);
		}
	}
return lc($id);
}

# sync_webmin_ssl_cert(&domain, [enable-or-disable])
# Add or remove the SSL cert for Webmin for this domain. Returns 1 on success,
# 0 on failure, or -1 if not supported. Calls restart_webmin_fully on older
# Webmin versions because SSL certs are only loaded by miniserv at startup,
# rather than on a config reload.
sub sync_webmin_ssl_cert
{
my ($d, $enable) = @_;
my %miniserv;
&get_miniserv_config(\%miniserv);
return -1 if (!$miniserv{'ssl'});
if ($enable) {
	return &setup_ipkeys($d, \&get_miniserv_config,
			     \&put_miniserv_config, \&restart_webmin);
	}
else {
	return &delete_ipkeys($d, \&get_miniserv_config,
			      \&put_miniserv_config, \&restart_webmin);
	}
}

# sync_usermin_ssl_cert(&domain, [enable-or-disable])
# Add or remove the SSL cert for Usermin for this domain. Returns 1 on success,
# 0 on failure, or -1 if not supported.
sub sync_usermin_ssl_cert
{
my ($d, $enable) = @_;
return -1 if (!&foreign_installed("usermin"));
&foreign_require("usermin");
my %miniserv;
&usermin::get_miniserv_config(\%miniserv);
return -1 if (!$miniserv{'ssl'});
if ($enable) {
	return &setup_ipkeys($d, \&usermin::get_usermin_miniserv_config,
		      \&usermin::put_usermin_miniserv_config,
		      \&restart_usermin);
	}
else {
	return &delete_ipkeys($d, \&usermin::get_usermin_miniserv_config,
		       \&usermin::put_usermin_miniserv_config,
		       \&restart_usermin);
	}
}

# ssl_needs_apache_restart()
# Returns 1 if an SSL cert change needs an Apache restart
sub ssl_needs_apache_restart
{
&require_apache();
return $apache::httpd_modules{'core'} >= 2.4 ? 0 : 1;
}

# ssl_certificate_directories(&domain, [absolute])
# Returns dirs relative to the domain's home needed for SSL certs
sub ssl_certificate_directories
{
my ($d, $abs) = @_;
my @paths;
foreach my $t (&list_ssl_file_types()) {
	push(@paths, &default_certificate_file($d, $t));
	if ($d->{'ssl_'.$t}) {
		push(@paths, $d->{'ssl_'.$t});
		}
	}
my @rv;
foreach my $p (&unique(@paths)) {
	if (!$abs) {
		# Must be relative to home dir
		$p =~ s/^\Q$d->{'home'}\E\/// || next;
		}
	if ($p =~ /^(.*)\//) {
		push(@rv, $1);
		}
	}
return @rv;
}

# create_ssl_certificate_directories(&domain)
# Create all dirs needed for SSL certs
sub create_ssl_certificate_directories
{
my ($d) = @_;
foreach my $dir (&ssl_certificate_directories($d, 1)) {
	if (&is_under_directory($d->{'home'}, $dir)) {
		# Create in the home dir, owned by the user
		&create_standard_directory_for_domain($d, $dir, '700')
			if (!-d $dir);
		}
	else {
		# Create elsewhere if needed. Should *not* be writable by the
		# domain user, to prevent cert deletion.
		if (!-d $dir) {
			&make_dir($dir, 0700, 1);
			}
		}
	}
}

# refresh_ssl_cert_expiry(&domain)
# Update the ssl_cert_expiry field from the actual cert
sub refresh_ssl_cert_expiry
{
my ($d) = @_;
my $cert_info = &cert_info($d);
if ($cert_info) {
	my $expiry = &parse_notafter_date($cert_info->{'notafter'});
	if ($expiry) {
		$d->{'ssl_cert_expiry'} = $expiry;
		}
	}
}

# get_ssl_cert_expiry(&domain)
# Returns the cached SSL cert expiry for a domain
sub get_ssl_cert_expiry
{
my ($d) = @_;
if ($d->{'ssl_same'}) {
	my $s = &get_domain($d->{'ssl_same'});
	return $s ? &get_ssl_cert_expiry($s) : undef;
	}
return $d->{'ssl_cert_expiry'};
}

# can_reset_ssl(&domain)
# Resetting SSL on it's own doesn't make sense, since it's included in the web
# feature reset
sub can_reset_ssl
{
return 0;
}

# show_template_ssl(&tmpl)
# Outputs HTML for editing SSL related template options
sub show_template_ssl
{
my ($tmpl) = @_;

# SSL cert size
print &ui_table_row(
	&hlink($text{'newweb_ssl_key_size'}, "config_key_size"),
	&ui_opt_textbox("ssl_key_size", $tmpl->{'ssl_key_size'}, 6,
			$tmpl->{'default'} ? $text{'newweb_ssl_key_def'}
					   : $text{'tmpl_default'}).
		" ".$text{'dkim_bits'});

# SSL cert type
print &ui_table_row(
	&hlink($text{'newweb_cert_type'}, "config_cert_type"),
	&ui_radio("ssl_cert_type", $tmpl->{'ssl_cert_type'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 'sha1', 'SHA1' ],
	    [ 'sha2', 'SHA2' ],
	    [ 'ec', 'ECC' ] ]));

# Generate SSL certificate for domains without SSL website?
print &ui_table_row(
	&hlink($text{'newweb_always_ssl'}, "config_always_ssl"),
	&ui_radio("ssl_always_ssl", $tmpl->{'ssl_always_ssl'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 1, $text{'yes'} ],
	    [ 0, $text{'no'} ] ]));

# Default SSL key and cert file paths
foreach my $t ("key", "cert", "ca", "combined", "everything") {
	my $v = $tmpl->{'cert_'.$t.'_tmpl'};
	my $mode = $v eq "auto" ? 2 : $v ? 0 : 1;
	my @opts = ( [ 1, $text{'newweb_cert_def'} ] );
	if ($t ne "key") {
		push(@opts, [ 2, $text{'newweb_cert_auto'} ]);
		}
	push(@opts, [ 0, $text{'newweb_cert_file'},
		      &ui_textbox("web_cert_".$t, $mode == 0 ? $v : "", 50) ]);
	print &ui_table_row(
		&hlink($text{'newweb_cert_'.$t}, "config_".$t."_tmpl"),
		&ui_radio_table("web_certmode_".$t, $mode, \@opts));
	}

print &ui_table_hr();

# SSL cert provider
if (defined(&list_acme_providers)) {
	print &ui_table_row(
		&hlink($text{'newweb_acme'}, "template_web_acme"),
		&ui_select("web_acme", $tmpl->{'web_acme'},
		   [ [ "", $text{'default'} ],
		     map { [ $_->{'id'}, $_->{'desc'} || $_->{'issuer'} ] }
			 &list_acme_providers() ]));
	}

# Request cert from provider at domain creation time?
print &ui_table_row(
	&hlink($text{'newweb_auto_letsencrypt'}, "config_auto_letsencrypt"),
	&ui_radio("ssl_auto_letsencrypt", $tmpl->{'ssl_auto_letsencrypt'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 2, $text{'newweb_auto_letsencrypt2'} ],
	    [ 1, $text{'yes'} ],
	    [ 0, $text{'no'} ] ]));

# Request wildcard cert from provider?
print &ui_table_row(
	&hlink($text{'newweb_letsencrypt_wild'}, "config_letsencrypt_wild"),
	&ui_radio("ssl_letsencrypt_wild", $tmpl->{'ssl_letsencrypt_wild'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 1, $text{'yes'} ],
	    [ 0, $text{'no'} ] ]));

# Days before expiry to auto-renew?
print &ui_table_row(
	&hlink($text{'newweb_renew_letsencrypt'}, "config_renew_letsencrypt"),
	&ui_opt_textbox("ssl_renew_letsencrypt",
		$tmpl->{'ssl_renew_letsencrypt'},
		6, $tmpl->{'default'} ? $text{'newweb_renew_letsencrypt_def'}
				      : $text{'tmpl_default'}));

# Allow request for a subset of hostnames?
print &ui_table_row(
	&hlink($text{'newweb_allow_subset'}, "config_allow_subset"),
	&ui_radio("ssl_allow_subset", $tmpl->{'ssl_allow_subset'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 1, $text{'yes'} ],
	    [ 0, $text{'no'} ] ]));

# Default validation mode before SSL cert request
if (defined(&check_domain_connectivity)) {
	print &ui_table_row(
	    &hlink($text{'newweb_ssl_connectivity'}, "config_ssl_connectivity"),
	    &ui_radio("ssl_connectivity",
	      $tmpl->{'ssl_connectivity'},
	      [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	        [ 2, $text{'cert_connectivity2'} ],
	        [ 1, $text{'cert_connectivity1'} ],
	        [ 0, $text{'cert_connectivity0'} ] ]));
	}

# Default email notification mode
print &ui_table_row(
	&hlink($text{'newweb_ssl_email'}, "config_ssl_email"),
	&ui_radio("ssl_email", $tmpl->{'ssl_email'},
	     [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	       [ 0, $text{'yes'} ],
	       [ 1, $text{'cert_letsemailerr'} ],
	       [ 2, $text{'no'} ] ])."<br>\n".
	&ui_checkbox("ssl_email_master", 1,
		     $text{'cert_letsemail_master'},
                     $tmpl->{'ssl_email_master'}));

# Generate TLSA DNS records?
print &ui_table_row(
	&hlink($text{'newweb_tlsa_records'}, "config_tlsa_records"),
	&ui_radio("ssl_tlsa_records", $tmpl->{'ssl_tlsa_records'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 1, $text{'newweb_tlsa_records1'} ],
	    [ 2, $text{'newweb_tlsa_records2'} ],
	    [ 0, $text{'no'} ] ]));

# Combined SSL cert file?
print &ui_table_row(
	&hlink($text{'newweb_combined_cert'}, "config_combined_cert"),
	&ui_radio("ssl_combined_cert", $tmpl->{'ssl_combined_cert'},
	  [ $tmpl->{'default'} ? ( ) : ( [ '', $text{'tmpl_default'} ] ),
	    [ 2, $text{'newweb_combined_cert2'} ],
	    [ 1, $text{'newweb_combined_cert1'} ],
	    [ 0, $text{'newweb_combined_cert0'} ] ]));

print &ui_table_hr();

# Setup matching Webmin/Usermin SSL certs
print &ui_table_row(&hlink($text{'newweb_webmin'},
			   "template_web_webmin_ssl"),
	&ui_yesno_radio("web_webmin_ssl", $tmpl->{'web_webmin_ssl'}));

print &ui_table_row(&hlink($text{'newweb_usermin'},
			   "template_web_usermin_ssl"),
	&ui_yesno_radio("web_usermin_ssl", $tmpl->{'web_usermin_ssl'}));

# Setup Dovecot, Postfix and MySQL SSL certs
print &ui_table_row(&hlink($text{'newweb_dovecot'},
			   "template_web_dovecot_ssl"),
	&ui_yesno_radio("web_dovecot_ssl", $tmpl->{'web_dovecot_ssl'}));

print &ui_table_row(&hlink($text{'newweb_postfix'},
			   "template_web_postfix_ssl"),
	&ui_yesno_radio("web_postfix_ssl", $tmpl->{'web_postfix_ssl'}));

print &ui_table_row(&hlink($text{'newweb_mysql'},
			   "template_web_mysql_ssl"),
	&ui_yesno_radio("web_mysql_ssl", $tmpl->{'web_mysql_ssl'}));

}

# parse_template_ssl(&tmpl)
# Updates SSL related template options from %in
sub parse_template_ssl
{
my ($tmpl) = @_;

# Save key size
if ($in{'ssl_key_size_def'}) {
	delete($tmpl->{'ssl_key_size'});
	}
else {
	$in{'ssl_key_size'} =~ /^\d+$/ || &error($text{'newweb_essl_key_size'});
	$tmpl->{'ssl_key_size'} = $in{'ssl_key_size'};
	}

# Save cert hash
$tmpl->{'ssl_cert_type'} = $in{'ssl_cert_type'};

# Save key file templates
foreach my $t ("key", "cert", "ca", "combined", "everything") {
	my $mode = $in{'web_certmode_'.$t};
	my $v;
	if ($mode == 2) {
		$v = "auto";
		}
	elsif ($mode == 0) {
		$v = $in{'web_cert_'.$t};
		$v =~ /\S/ || &error($text{'newweb_cert_efile'});
		}
	$tmpl->{'cert_'.$t.'_tmpl'} = $v;
	}

# Save SSL provider options
if (defined($in{'web_acme'})) {
	$tmpl->{'web_acme'} = $in{'web_acme'};
	}
$tmpl->{'ssl_auto_letsencrypt'} = $in{'ssl_auto_letsencrypt'};
$tmpl->{'ssl_letsencrypt_wild'} = $in{'ssl_letsencrypt_wild'};
$tmpl->{'ssl_allow_subset'} = $in{'ssl_allow_subset'};
if (defined($in{'ssl_connectivity'})) {
	$tmpl->{'ssl_connectivity'} = $in{'ssl_connectivity'};
	}
$tmpl->{'ssl_email'} = $in{'ssl_email'};
$tmpl->{'ssl_email_master'} = $in{'ssl_email_master'};
if ($in{'ssl_renew_letsencrypt_def'}) {
	delete($tmpl->{'ssl_renew_letsencrypt'});
	}
else {
	$in{'ssl_renew_letsencrypt'} =~ /^\d+$/ ||
		&error($text{'newweb_erenew_letsencrypt'});
	$tmpl->{'ssl_renew_letsencrypt'} = $in{'ssl_renew_letsencrypt'};
	}
$tmpl->{'ssl_always_ssl'} = $in{'ssl_always_ssl'};

if ($in{'ssl_tlsa_records'}) {
	# Make sure TLSA records can be created
	my $err = &check_tlsa_support();
	&error(&text('check_etlsa', $err)) if ($err);
	}
$tmpl->{'ssl_tlsa_records'} = $in{'ssl_tlsa_records'};
$tmpl->{'ssl_combined_cert'} = $in{'ssl_combined_cert'};

# Save options to setup per-service SSL certs
$tmpl->{'web_webmin_ssl'} = $in{'web_webmin_ssl'};
$tmpl->{'web_usermin_ssl'} = $in{'web_usermin_ssl'};
$tmpl->{'web_postfix_ssl'} = $in{'web_postfix_ssl'};
$tmpl->{'web_dovecot_ssl'} = $in{'web_dovecot_ssl'};
$tmpl->{'web_mysql_ssl'} = $in{'web_mysql_ssl'};
}

# chained_ssl(&domain, [&old-domain])
# SSL is automatically enabled when a website is, if set to always mode
# and if the website is just being turned on now.
sub chained_ssl
{
my ($d, $oldd) = @_;
if ($config{'ssl'} != 3) {
	# Not in auto mode, so don't touch
	return undef;
	}
elsif ($d->{'alias'}) {
	# Aliases never have their own SSL
	return undef;
	}
elsif (&domain_has_website($d)) {
	if (!$oldd || !&domain_has_website($oldd)) {
		# Turning on web, so turn on SSL
		return 1;
		}
	else {
		# Don't do anything
		return undef;
		}
	}
else {
	# Always off when web is
	return 0;
	}
}

# associate_ssl(&domain)
# Called after the SSL feature is associated to setup the various ssl_ fields
sub associate_ssl
{
my ($d) = @_;
$d->{'ssl_cert'} = &get_website_ssl_file($d, 'cert') || $d->{'ssl_cert'};
$d->{'ssl_key'} = &get_website_ssl_file($d, 'key') || $d->{'ssl_key'};
$d->{'ssl_chain'} = &get_website_ssl_file($d, 'ca') || $d->{'ssl_chain'};
if (!$d->{'ssl_combined'} || !$d->{'ssl_everything'}) {
	&sync_combined_ssl_cert($d);
	}
return undef;
}

# can_chained_ssl()
# Returns the web feature because the SSL feature will be enabled if a
# website is
sub can_chained_ssl
{
return ('web');
}

# write_ssl_file_contents(&domain, file, contents|srcfile, [no-error])
# Write out an SSL key or cert file with the correct permissions.
# Returns undef on success, or an error message on failure if no-error is set.
sub write_ssl_file_contents
{
my ($d, $file, $contents, $noerror) = @_;
if (defined($contents) && $contents =~ /^\// && -r $contents) {
	# Actually copy from a file
	my $srcfile = $contents;
	$contents = &read_file_contents($srcfile);
	if (!defined($contents)) {
		return "Failed to read $srcfile" if ($noerror);
		&error("Failed to read $srcfile");
		}
	}
my $newfile = !-r $file;
if (&is_under_directory($d->{'home'}, $file)) {
	# Assume write can be done as the domain owner
	&disable_quotas($d);
	my $ok = &open_tempfile_as_domain_user($d, KEY, ">$file", $noerror);
	if (!$ok) {
		&enable_quotas($d);
		return "Failed to open $file for writing" if ($noerror);
		&error("Failed to open $file for writing");
		}
	&print_tempfile(KEY, $contents);
	$ok = &close_tempfile_as_domain_user($d, KEY);
	&enable_quotas($d);
	if (!$ok) {
		return "Failed to write $file" if ($noerror);
		&error("Failed to write $file");
		}
	&set_certificate_permissions($d, $file) if ($newfile);
	}
else {
	# If SSL cert is elsewhere (like /etc/ssl), write as root
	my $ok = &open_tempfile(KEY, ">$file", $noerror);
	if (!$ok) {
		return "Failed to open $file for writing : $!" if ($noerror);
		&error("Failed to open $file for writing : $!");
		}
	&print_tempfile(KEY, $contents);
	$ok = &close_tempfile(KEY);
	if (!$ok) {
		return "Failed to write $file : $!" if ($noerror);
		&error("Failed to write $file : $!");
		}
	&set_ownership_permissions(undef, undef, 0600, $file);
	}
return undef;
}

# get_dovecot_ssl_dir(cert|key, file)
# Returns the name and value of the correct Dovecot SSL directive for
# this version
sub get_dovecot_ssl_dir
{
my ($type, $file) = @_;
if (&dovecot::version_atleast(2.4)) {
	return ("ssl_server_".$type."_file", $file);
	}
else {
	return ("ssl_".$type, "<".$file);
	}
}

# rerequest_cert_on_domain_change(&domain, &old-domain)
# If a virutal server's domain name changes, re-request or re-generate the SSL
# cert if possible
sub rerequest_cert_on_domain_change
{
my ($d, $oldd) = @_;
my $rv = 0;

if ($d->{'dom'} ne $oldd->{'dom'} && &self_signed_cert($d) &&
    !&check_domain_certificate($d->{'dom'}, $d)) {
	# Domain name has changed .. re-generate self-signed cert
	&$first_print($text{'save_ssl11'});
	my $info = &cert_info($d);
	&lock_file($d->{'ssl_cert'});
	&lock_file($d->{'ssl_key'});
	my @newalt = $info->{'alt'} ? @{$info->{'alt'}} : ( );
	foreach my $a (@newalt) {
		if ($a eq $oldd->{'dom'}) {
			$a = $d->{'dom'};
			}
		elsif ($a =~ /^([^\.]+)\.(\S+)$/ && $2 eq $oldd->{'dom'}) {
			$a = $1.".".$d->{'dom'};
			}
		}
	my $email = $info->{'emailAddress'};
	$email =~ s/\@\Q$oldd->{'dom'}\E$/\@$d->{'dom'}/;
	my $err = &generate_self_signed_cert(
		$d->{'ssl_cert'}, $d->{'ssl_key'},
		undef,
		1825,
		$info->{'c'},
		$info->{'st'},
		$info->{'l'},
		$info->{'o'},
		$info->{'ou'},
		"*.$d->{'dom'}",
		$email,
		\@newalt,
		$d,
		);
	&unlock_file($d->{'ssl_key'});
	&unlock_file($d->{'ssl_cert'});
	if ($err) {
		&$second_print(&text('setup_eopenssl', $err));
		}
	else {
		$rv++;
		&$second_print($text{'setup_done'});
		}
	}

if ($d->{'dom'} ne $oldd->{'dom'} &&
    !$d->{'ssl_same'} &&
    &is_acme_cert($d) &&
    !&check_domain_certificate($d->{'dom'}, $d)) {
	# Domain name has changed ... re-request let's encrypt cert
	&$first_print($text{'save_ssl12'});
	if ($d->{'letsencrypt_dname'}) {
		# Update any explicitly chosen domain names
		my @dnames = split(/\s+/, $d->{'letsencrypt_dname'});
		foreach my $dn (@dnames) {
			$dn = $d->{'dom'} if ($dn eq $oldd->{'dom'});
			$dn =~ s/\.\Q$oldd->{'dom'}\E$/\.$d->{'dom'}/;
			}
		$d->{'letsencrypt_dname'} = join(" ", @dnames);
		}
	my ($ok, $err) = &renew_letsencrypt_cert($d);
	if ($ok) {
		&$second_print($text{'setup_done'});
		}
	else {
		&$second_print(&text('save_essl12', $err));
		}
	}

return $rv;
}

# update_ssl_link_on_domain_change(&domain, &old-domain)
# If a virtual server changed IP address, update SSL cert linkage
sub update_ssl_link_on_domain_change
{
my ($d, $oldd) = @_;
my $ipkey = $d->{'ip'} ? 'ip' : 'ip6';
my $oldipkey = $oldd->{'ip'} ? 'ip' : 'ip6';
my $ip = $d->{$ipkey};
my $oldip = $oldd->{$oldipkey};
if (!$ip) {
	# With no IP address, SSL cert linkage is not possible
	if ($oldd->{'ssl_same'}) {
		my $oldsslclash = &get_domain($oldd->{'ssl_same'});
		&break_ssl_linkage($d, $oldsslclash);
		}
	}
elsif (($ipkey ne $oldipkey || !$oldip || $ip ne $oldip) &&
       $oldd->{'ssl_same'}) {
	# IP has changed or been added - maybe clear ssl_same field
	my ($sslclash) = grep { $_->{$ipkey} && $_->{$ipkey} eq $ip &&
				&domain_has_ssl($_) &&
				$_->{'id'} ne $d->{'id'} &&
				!$_->{'ssl_same'} } &list_domains();
	my $oldsslclash = &get_domain($oldd->{'ssl_same'});
	if ($sslclash && $oldd->{'ssl_same'} eq $sslclash->{'id'}) {
		# No need to change
		}
	elsif ($sslclash &&
	       &check_domain_certificate($d->{'dom'}, $sslclash)) {
		# New domain with same cert
		$d->{'ssl_cert'} = $sslclash->{'ssl_cert'};
		$d->{'ssl_key'} = $sslclash->{'ssl_key'};
		$d->{'ssl_same'} = $sslclash->{'id'};
		my $chained = &get_website_ssl_file($sslclash, 'ca');
		$d->{'ssl_chain'} = $chained;
		$d->{'ssl_combined'} = $sslclash->{'ssl_combined'};
		$d->{'ssl_everything'} = $sslclash->{'ssl_everything'};
		}
	else {
		# No domain has the same cert anymore - copy the one from the
		# old sslclash domain
		&break_ssl_linkage($d, $oldsslclash);
		}
	}
}

$done_feature_script{'ssl'} = 1;

1;
