From 882b694e06acd389dd74f7a7d9b70ada0fd218d5 Mon Sep 17 00:00:00 2001 From: Simon Rettberg Date: Tue, 8 Oct 2024 16:22:17 +0200 Subject: [webinterface] Add support for ACME, add option to redirect to cert domain --- inc/taskmanagercallback.inc.php | 9 + inc/util.inc.php | 40 +++++ index.php | 52 ++++-- .../main/templates/domain-redirect-check.html | 13 ++ modules-available/webinterface/hooks/cron.inc.php | 5 + .../webinterface/hooks/main-warning.inc.php | 8 + modules-available/webinterface/inc/acme.inc.php | 164 ++++++++++++++++++ .../webinterface/inc/webinterface.inc.php | 65 ++++++++ .../webinterface/lang/de/messages.json | 8 +- .../webinterface/lang/de/template-tags.json | 19 +++ .../webinterface/lang/en/messages.json | 8 +- .../webinterface/lang/en/template-tags.json | 19 +++ modules-available/webinterface/page.inc.php | 184 ++++++++++++++++----- .../webinterface/templates/acme-error.html | 5 + .../webinterface/templates/heading.html | 5 + .../webinterface/templates/httpd-restart.html | 4 +- .../webinterface/templates/https.html | 150 ++++++++++++++--- 17 files changed, 669 insertions(+), 89 deletions(-) create mode 100644 modules-available/main/templates/domain-redirect-check.html create mode 100644 modules-available/webinterface/hooks/cron.inc.php create mode 100644 modules-available/webinterface/hooks/main-warning.inc.php create mode 100644 modules-available/webinterface/inc/acme.inc.php create mode 100644 modules-available/webinterface/inc/webinterface.inc.php create mode 100644 modules-available/webinterface/templates/acme-error.html diff --git a/inc/taskmanagercallback.inc.php b/inc/taskmanagercallback.inc.php index c6b447c9..9f276020 100644 --- a/inc/taskmanagercallback.inc.php +++ b/inc/taskmanagercallback.inc.php @@ -240,4 +240,13 @@ class TaskmanagerCallback SystemStatus::setUpgradableData($task); } + public static function acmeErrors(array $task): void + { + $mod = Module::get('webinterface'); + if ($mod === false) + return; + $mod->activate(1, false); + Acme::callbackErrorCheck($task); + } + } diff --git a/inc/util.inc.php b/inc/util.inc.php index 0ff6fdad..ed0b40ae 100644 --- a/inc/util.inc.php +++ b/inc/util.inc.php @@ -471,4 +471,44 @@ class Util } } + public static function shouldRedirectDomain(): ?string + { + if ($_SERVER['HTTP_HOST'] === 'satellite.bwlehrpool') + return null; + if (!Property::get('webinterface.redirect-domain') || empty(Property::get('webinterface.https-domains'))) + return null; // Disabled, or unknown domain + $curDomain = $_SERVER['HTTP_HOST']; + if ($curDomain[-1] === '.') { + $curDomain = substr($curDomain, 0, -1); + } + $domains = explode(' ', Property::get('webinterface.https-domains')); + foreach ($domains as $domain) { + if ($domain === $curDomain) + return null; // MATCH + if (substr_compare($domain, '*.', 0, 2) === 0) { + if (substr($domain, 2) === $curDomain) + return null; // MATCH + $len = strlen($domain) - 1; + if (substr($curDomain, -$len) === substr($domain, 1)) + return null; // MATCH + } + } + // No match + return $domains[0]; + /* + $str = '//' . $domains[0] . '/' . $_SERVER['REQUEST_URI']; + if (!empty($_SERVER['QUERY_STRING'])) { + $str .= '?' . $_SERVER['QUERY_STRING']; + } + Header('Location: ' . $str); + exit; + */ + } + + public static function osUptime(): int + { + $a = file_get_contents('/proc/uptime'); + return (int)preg_replace('/[^0-9].*$/s', '', $a); + } + } diff --git a/index.php b/index.php index 6b5305b2..c8542b80 100644 --- a/index.php +++ b/index.php @@ -19,6 +19,33 @@ require_once('inc/user.inc.php'); $global_start = microtime(true); +// Set variable if this is an ajax request +if ((isset($_REQUEST['async'])) || (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')) { + define('AJAX', true); +} else { + define('AJAX', false); +} +define('API', false); + +// Autoload classes from ./inc which adhere to naming scheme .inc.php +spl_autoload_register(function ($class) { + $file = 'inc/' . preg_replace('/[^a-z0-9]/', '', mb_strtolower($class)) . '.inc.php'; + if (!file_exists($file)) + return; + require_once $file; +}); + +if (($_GET['do'] ?? '') === '_https_magic') { + Header('Access-Control-Allow-Origin: *'); + Header('Content-Type: application/json'); + $ut = floor(Util::osUptime() / 3); + $str = Property::getServerIp() . serialize(Property::getVmStoreConfig()); + die(json_encode([ + 'a' => md5($ut . $str), + 'b' => md5(($ut - 1) . $str), + ])); +} + /** * Page class which all module's pages must be extending from */ @@ -90,23 +117,6 @@ abstract class Page } -// Set variable if this is an ajax request -if ((isset($_REQUEST['async'])) || (!empty($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest')) { - define('AJAX', true); -} else { - define('AJAX', false); -} -define('API', false); - -// Autoload classes from ./inc which adhere to naming scheme .inc.php -spl_autoload_register(function ($class) { - $file = 'inc/' . preg_replace('/[^a-z0-9]/', '', mb_strtolower($class)) . '.inc.php'; - if (!file_exists($file)) - return; - require_once $file; -}); - - if (defined('CONFIG_DEBUG') && CONFIG_DEBUG) { set_error_handler(function ($errno, $errstr, $errfile, $errline) { if (preg_match('/^\[skip:\s*(\d+)\]\s*(.*)/is', $errstr, $out)) { @@ -237,5 +247,13 @@ if (CONFIG_DEBUG) { ), 'main'); } +// Redirect if not accessed via proper domain +if ($_SERVER['REQUEST_METHOD'] === 'GET' && ($host = Util::shouldRedirectDomain()) !== null) { + Render::addTemplate('domain-redirect-check', [ + 'magic' => md5((string)floor(Util::osUptime() / 3) . Property::getServerIp() . serialize(Property::getVmStoreConfig())), + 'host' => $host, + ], 'main'); +} + // Send page to client. Render::output(); diff --git a/modules-available/main/templates/domain-redirect-check.html b/modules-available/main/templates/domain-redirect-check.html new file mode 100644 index 00000000..6019c045 --- /dev/null +++ b/modules-available/main/templates/domain-redirect-check.html @@ -0,0 +1,13 @@ + \ No newline at end of file diff --git a/modules-available/webinterface/hooks/cron.inc.php b/modules-available/webinterface/hooks/cron.inc.php new file mode 100644 index 00000000..cc30ed05 --- /dev/null +++ b/modules-available/webinterface/hooks/cron.inc.php @@ -0,0 +1,5 @@ + "Let's Encrypt", + 'zerossl' => 'ZeroSSL.com', + 'buypass' => 'BuyPass.com', + 'geant/sectigo' => 'GEANT via Sectigo', + ]; + + const PROVIDER_ALIASES = [ + 'geant/sectigo' => 'https://acme.sectigo.com/v2/GEANTOV', + ]; + + public static function getLastError(): ?string + { + return Property::get(self::PROP_ERROR, null); + } + + public static function getProvider(): ?string + { + return Property::get(self::PROP_PROVIDER, null); + } + + public static function getKeyId(): ?string + { + return Property::get(self::PROP_KEY_ID, null); + } + + public static function getHmacKey(): ?string + { + return Property::get(self::PROP_HMAC_KEY, null); + } + + /** + * @return string[] list of [id] => friendly name + */ + public static function getProviders(): array + { + return self::VALID_PROVIDERS; + } + + public static function getMail(): ?string + { + return Property::get(self::PROP_MAIL, null); + } + + public static function getDomains(): array + { + return explode(' ', Property::get(self::PROP_DOMAINS)); + } + + public static function setConfig(string $provider, string $mail, ?string $keyId = null, ?string $hmacKey = null): bool + { + if (!isset(self::VALID_PROVIDERS[$provider])) { + Message::addError('webinterface.acme-invalid-provider', $provider); + return false; + } + Property::set(self::PROP_PROVIDER, $provider); + Property::set(self::PROP_MAIL, $mail); + Property::set(self::PROP_KEY_ID, $keyId); + Property::set(self::PROP_HMAC_KEY, $hmacKey); + return true; + } + + public static function setDomains(array $list): void + { + Property::set(self::PROP_DOMAINS, implode(' ', $list)); + } + + private static function handleErrorAsync($task): void + { + if (!is_array($task) || !Taskmanager::isTask($task)) + return; + $task = Taskmanager::waitComplete($task, 250); + if (Taskmanager::isFinished($task)) { + self::callbackErrorCheck($task); + } else { + Property::set(self::PROP_ERROR, false); + TaskmanagerCallback::addCallback($task, 'acmeErrors'); + } + } + + public static function callbackErrorCheck(array $task): void + { + if (!Taskmanager::isFinished($task)) + return; + if (Taskmanager::isFailed($task)) { + Property::set(self::PROP_ERROR, $task['data']['error'] ?? 'Unknown error'); + } else { + Property::set(self::PROP_ERROR, false); + } + } + + public static function issueNewCertificate(bool $wipeAll = false): ?string + { + $provider = self::getProvider(); + if ($provider === null) { + Message::addError('webinterface.acme-no-provider'); + return null; + } + $mail = self::getMail(); + if (empty($mail)) { + Message::addError('webinterface.acme-no-mail'); + return null; + } + $domains = self::getDomains(); + if (empty($domains)) { + Message::addError('webinterface.acme-no-domains'); + return null; + } + $redirect = Property::get(WebInterface::PROP_REDIRECT); + $task = Taskmanager::submit('LighttpdHttps', [ + 'redirect' => $redirect, + 'acmeMode' => 'issue', + 'acmeMail' => $mail, + 'acmeDomains' => $domains, + 'acmeProvider' => self::PROVIDER_ALIASES[$provider] ?? $provider, + 'acmeKeyId' => self::getKeyId(), + 'acmeHmacKey' => self::getHmacKey(), + 'acmeWipeAll' => $wipeAll, + ]); + self::handleErrorAsync($task); + return $task['id'] ?? null; + } + + public static function renew(): ?string + { + error_log("Renew called"); + $domains = self::getDomains(); + if (empty($domains)) { + Message::addError('webinterface.acme-no-domains'); + return null; + } + $redirect = Property::get(WebInterface::PROP_REDIRECT); + $task = Taskmanager::submit('LighttpdHttps', [ + 'redirect' => $redirect, + 'acmeMode' => 'renew', + 'acmeDomains' => $domains, + ]); + self::handleErrorAsync($task); + return $task['id'] ?? null; + } + + public static function tryEnable(): bool + { + $redirect = Property::get(WebInterface::PROP_REDIRECT); + $task = Taskmanager::submit('LighttpdHttps', [ + 'redirect' => $redirect, + 'acmeMode' => 'try-enable', + ]); + $task = Taskmanager::waitComplete($task, 10000); + return Taskmanager::isFinished($task) && !Taskmanager::isFailed($task); + } + +} \ No newline at end of file diff --git a/modules-available/webinterface/inc/webinterface.inc.php b/modules-available/webinterface/inc/webinterface.inc.php new file mode 100644 index 00000000..6dfd924f --- /dev/null +++ b/modules-available/webinterface/inc/webinterface.inc.php @@ -0,0 +1,65 @@ +actionCustomization(); break; + default: } if (Request::isPost()) { Util::redirect('?do=webinterface'); @@ -38,24 +35,28 @@ class Page_WebInterface extends Page { $mode = Request::post('mode'); switch ($mode) { - case 'off': - $task = $this->setHttpsOff(); - break; - case 'random': - $task = $this->setHttpsRandomCert(); - break; - case 'custom': - $task = $this->setHttpsCustomCert(); - break; - default: - $task = $this->setRedirectMode(); - break; + case 'off': + $taskId = $this->setHttpsOff(); + break; + case 'random': + $taskId = $this->setHttpsRandomCert(); + break; + case 'custom': + $taskId = $this->setHttpsCustomCert(); + break; + case 'acme': + $taskId = $this->setAcmeMode(); + break; + default: + $taskId = $this->setRedirectMode(); + break; } if ($mode !== 'off') { - Property::set(self::PROP_HSTS, Request::post('usehsts', false, 'string') === 'on' ? 'True' : 'False'); + Property::set(WebInterface::PROP_HSTS, Request::post('usehsts', false, 'string') === 'on' ? 'True' : 'False'); + WebInterface::setDomainRedirect(Request::post('redirdomain', false, 'string') === 'on'); } - if (isset($task['id'])) { - Session::set('https-id', $task['id']); + if ($taskId !== null) { + Session::set('https-id', $taskId, 1); Util::redirect('?do=WebInterface&show=httpsupdate'); } Util::redirect('?do=WebInterface'); @@ -70,7 +71,7 @@ class Page_WebInterface extends Page private function actionCustomization() { $prefix = Request::post('prefix', '', 'string'); - if (!empty($prefix) && !preg_match('/[\]\)\}\-_\s\&\$\!\/\+\*\^\>]$/', $prefix)) { + if (!empty($prefix) && !preg_match('/[)}\]\-_\s&$!\/+*^>]$/', $prefix)) { $prefix .= ' '; } Property::set('page-title-prefix', $prefix); @@ -87,17 +88,25 @@ class Page_WebInterface extends Page if (Request::get('show') === 'httpsupdate') { Render::addTemplate('httpd-restart', array('taskid' => Session::get('https-id'))); } - $type = Property::get(self::PROP_TYPE); - $force = Property::get(self::PROP_REDIRECT) === 'True'; - $hsts = Property::get(self::PROP_HSTS) === 'True'; + $type = Property::get(WebInterface::PROP_TYPE); + $force = Property::get(WebInterface::PROP_REDIRECT) === 'True'; + $hsts = Property::get(WebInterface::PROP_HSTS) === 'True'; + $redirdomain = WebInterface::getDomainRedirect(); $https = !empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off'; $exists = file_exists('/etc/lighttpd/server.pem'); $data = array( 'httpsUsed' => $https, 'redirect_checked' => ($force ? 'checked' : ''), - 'hsts_checked' => ($hsts ? 'checked' : '') + 'hsts_checked' => ($hsts ? 'checked' : ''), + 'redirdomain_checked' => ($redirdomain ? 'checked' : ''), ); - // Type should be 'off', 'generated', 'supplied' + // Type should be 'off', 'generated', 'supplied' or 'acme' + if ($type === 'acme') { + $err = Acme::getLastError(); + if (!empty($err)) { + Render::addTemplate('acme-error', ['error' => $err]); + } + } if ($type === 'off') { if ($exists) { // HTTPS is set to off, but a certificate exists @@ -113,7 +122,7 @@ class Page_WebInterface extends Page // Admin might have modified web server config in another way Message::addWarning('https-used-without-cert'); } - } elseif ($type === 'generated' || $type === 'supplied') { + } elseif ($type === 'generated' || $type === 'supplied' || $type === 'acme') { $data['httpsEnabled'] = true; if ($force && !$https) { Message::addWarning('https-want-redirect-is-plain'); @@ -125,21 +134,59 @@ class Page_WebInterface extends Page // Unknown config - maybe upgraded old install that doesn't keep track if ($exists || $https) { $type = 'unknown'; // Legacy fallback + $data['httpsEnabled'] = true; } else { $type = 'off'; } } + $domains = implode("\n", Acme::getDomains()); + if (empty($domains)) { + $domains = $_SERVER['HTTP_HOST']; + } + $data['acmeProviders'] = []; + foreach (Acme::getProviders() as $id => $name) { + $data['acmeProviders'][] = [ + 'id' => $id, + 'name' => $name, + 'selected' => $id === Acme::getProvider() ? 'selected' : '', + ]; + } + $data['acmeMail'] = Acme::getMail(); + $data['acmeDomains'] = $domains; + $data['acmeKeyId'] = Acme::getKeyId(); + $data['acmeHmacKey'] = Acme::getHmacKey(); + // $type might have changed in above block $data[$type . 'Selected'] = true; + // Show cert info if possible + if ($type !== 'off') { + $data['certDomains'] = []; + $exp = 0; + $iss = ''; + if (WebInterface::extractCurrentCertData($data['certDomains'], $exp, $iss)) { + $data['certExpire'] = Util::prettyTime($exp); + $data['certIssuer'] = $iss; + $diff = $exp - time(); + $class = []; + if ($diff < 86400 * 3) { + $class[] = 'text-danger'; + } + if ($diff < 86400 * 10) { + $class[] = 'slx-bold'; + } + $data['certExpireClass'] = implode(' ', $class); + } + } Permission::addGlobalTags($data['perms'], null, ['edit.https']); Render::addTemplate('https', $data); // // Password fields // $data = array(); - if (Property::getPasswordFieldType() === 'text') + if (Property::getPasswordFieldType() === 'text') { $data['selected_show'] = 'checked'; - else + } else { $data['selected_hide'] = 'checked'; + } Permission::addGlobalTags($data['perms'], null, ['edit.password']); Render::addTemplate('passwords', $data); // @@ -164,52 +211,99 @@ class Page_WebInterface extends Page Render::addTemplate('customization', $data); } - private function setHttpsOff() + private function setHttpsOff(): ?string { - Property::set(self::PROP_TYPE, 'off'); - Property::set(self::PROP_HSTS, 'off'); + Property::set(WebInterface::PROP_TYPE, 'off'); + Property::set(WebInterface::PROP_HSTS, 'off'); Header('Strict-Transport-Security: max-age=0', true); Session::deleteCookie(); - return Taskmanager::submit('LighttpdHttps', array()); + $task = Taskmanager::submit('LighttpdHttps', array()); + return $task['id'] ?? null; } - private function setHttpsRandomCert() + private function setHttpsRandomCert(): ?string { $force = Request::post('httpsredirect', false, 'string') === 'on'; - Property::set(self::PROP_TYPE, 'generated'); - Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False'); - return Taskmanager::submit('LighttpdHttps', array( + Property::set(WebInterface::PROP_TYPE, 'generated'); + Property::set(WebInterface::PROP_REDIRECT, $force ? 'True' : 'False'); + $task = Taskmanager::submit('LighttpdHttps', array( 'proxyip' => Property::getServerIp(), 'redirect' => $force, )); + return $task['id'] ?? null; } - private function setHttpsCustomCert() + private function setHttpsCustomCert(): ?string { $force = Request::post('httpsredirect', false, 'string') === 'on'; - Property::set(self::PROP_TYPE, 'supplied'); - Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False'); - return Taskmanager::submit('LighttpdHttps', array( + Property::set(WebInterface::PROP_TYPE, 'supplied'); + Property::set(WebInterface::PROP_REDIRECT, $force ? 'True' : 'False'); + $task = Taskmanager::submit('LighttpdHttps', array( 'importcert' => Request::post('certificate', 'bla'), 'importkey' => Request::post('privatekey', 'bla'), 'importchain' => Request::post('cachain', ''), 'redirect' => $force, )); + return $task['id'] ?? null; + } + + private function setAcmeMode(): ?string + { + $force = Request::post('httpsredirect', false, 'string') === 'on'; + Property::set(WebInterface::PROP_TYPE, 'acme'); + Property::set(WebInterface::PROP_REDIRECT, $force ? 'True' : 'False'); + $wipeAll = Request::post('acme-wipe-all', false, 'bool'); + // Get params + $provider = Request::post('acme-provider', Request::REQUIRED, 'string'); + $mail = Request::post('acme-mail', Request::REQUIRED, 'string'); + $domains = Request::post('acme-domains', Request::REQUIRED, 'string'); + $kid = Request::post('acme-kid', null, 'string'); + $hmac = Request::post('acme-hmac-key', null, 'string'); + // Check domains + $domains = preg_split('/[\r\n\s]+/', strtolower($domains), 0, PREG_SPLIT_NO_EMPTY); + $err = false; + foreach ($domains as $domain) { + if (!preg_match('/^[a-z0-9_.-]+$/', $domain)) { + Message::addError('invalid-domain', $domain); + $err = true; + } + } + unset($domain); + if ($err) + return null; + // First, try to revive existing config/certs if parameters didn't change + if (!$wipeAll + && $provider === Acme::getProvider() + && $mail === Acme::getMail() + && $kid === Acme::getKeyId() + && $hmac === Acme::getHmacKey() + && count($domains) === count(Acme::getDomains()) + && empty(array_diff($domains, Acme::getDomains()))) { + if (Acme::tryEnable()) + return null; // Nothing to do, old setup works + error_log('FUUUU'); + return Acme::renew(); // Hope for the best, otherwise user needs to check "force reissue" + } + if (!Acme::setConfig($provider, $mail, $kid, $hmac)) + return null; // Will generate error messages in this case + Acme::setDomains($domains); + return Acme::issueNewCertificate($wipeAll); } - private function setRedirectMode() + private function setRedirectMode(): ?string { $force = Request::post('httpsredirect', false, 'string') === 'on'; - Property::set(self::PROP_REDIRECT, $force ? 'True' : 'False'); - if (Property::get(self::PROP_TYPE) === 'off') { + Property::set(WebInterface::PROP_REDIRECT, $force ? 'True' : 'False'); + if (Property::get(WebInterface::PROP_TYPE) === 'off') { // Don't bother running the task if https isn't enabled - just // update the state in DB - return false; + return null; } - return Taskmanager::submit('LighttpdHttps', array( + $task = Taskmanager::submit('LighttpdHttps', array( 'redirectOnly' => true, 'redirect' => $force, )); + return $task['id'] ?? null; } } diff --git a/modules-available/webinterface/templates/acme-error.html b/modules-available/webinterface/templates/acme-error.html new file mode 100644 index 00000000..facbbad2 --- /dev/null +++ b/modules-available/webinterface/templates/acme-error.html @@ -0,0 +1,5 @@ +
+ {{lang_msgAcmeFailed}} +
+
{{error}}
+
\ No newline at end of file diff --git a/modules-available/webinterface/templates/heading.html b/modules-available/webinterface/templates/heading.html index 59a8cf6b..b759eea9 100644 --- a/modules-available/webinterface/templates/heading.html +++ b/modules-available/webinterface/templates/heading.html @@ -4,6 +4,7 @@ \ No newline at end of file diff --git a/modules-available/webinterface/templates/httpd-restart.html b/modules-available/webinterface/templates/httpd-restart.html index ac4e726b..75d86ad3 100644 --- a/modules-available/webinterface/templates/httpd-restart.html +++ b/modules-available/webinterface/templates/httpd-restart.html @@ -19,7 +19,9 @@ function slxRestartCb(task) { } else { console.log('Disabling because ' + task.statusCode); clearInterval(slxRedirTimer); - window.location.replace(window.location.href.replace('&show=httpsupdate', '')); + if (task.statusCode === 'TASK_FINISHED' && (!task.data || !task.data.error || !task.data.error.length)) { + window.location.replace(window.location.href.replace('&show=httpsupdate', '')); + } } } diff --git a/modules-available/webinterface/templates/https.html b/modules-available/webinterface/templates/https.html index ad36e9e5..5198c299 100644 --- a/modules-available/webinterface/templates/https.html +++ b/modules-available/webinterface/templates/https.html @@ -6,10 +6,10 @@

{{lang_httpsDescription}}

{{^httpsUsed}} - {{lang_youreNotUsingHttps}} +

{{lang_youreNotUsingHttps}}

{{/httpsUsed}} {{#httpsUsed}} - {{lang_youreUsingHttps}} +

{{lang_youreUsingHttps}}

{{/httpsUsed}}
{{#offSelected}} @@ -24,8 +24,80 @@ {{#suppliedSelected}}

{{lang_suppliedSelected}}

{{/suppliedSelected}} + {{#acmeSelected}} +

{{lang_acmeSelected}}

+ {{/acmeSelected}}
+ + {{#certIssuer}} + + {{/certIssuer}} + {{#certExpire}} + + {{/certExpire}} + {{#certDomains.0}} + + + + + {{/certDomains.0}} +
{{lang_certIssuer}}:{{.}}
{{lang_certExpireTime}}:{{.}}
{{lang_currentCertDomains}}: + {{#certDomains}} +
{{.}}
+ {{/certDomains}} +
+ + +
+ + + + + + + + {{lang_httpsRedirect}} + +
+
+ + + + + + + + {{lang_useHsts}} + +
+
+ + + + + + + + {{lang_redirectDomain}} + +
+
+ +
+ +
+ + + + + + + + {{lang_httpsOptionNoChange}} + +
+ {{#httpsEnabled}}
@@ -39,6 +111,7 @@
{{/httpsEnabled}} +
@@ -50,6 +123,7 @@ {{lang_randomCert}}
+
@@ -61,6 +135,18 @@ {{lang_customCert}}
+ +
+ + + + + + + + {{lang_optionAcme}} + +
@@ -85,29 +171,45 @@ MIIFfTCCA...
-
-
- - - - - - - - {{lang_httpsRedirect}} - -
-
- - - - - - - - {{lang_useHsts}} - +
+

{{lang_acmeCreateNewHint}}

+
+ + +
+
+ + +
+
+ + +
+
+
+ {{lang_acmeKidKeyHint}} +
+
+ + +
+
+ + +
+
+
+ + +
+ {{lang_acmeWipeAllHint}}
+
-- cgit v1.2.3-55-g7522