usinng acme.sh for issuing Let's Encrypt certificates now; please test thoroughly; fixes #651
Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
@@ -185,12 +185,18 @@ return array(
|
||||
'default' => 4096,
|
||||
'save_method' => 'storeSettingField'
|
||||
),
|
||||
'system_letsencryptreuseold' => array(
|
||||
'label' => $lng['serversettings']['letsencryptreuseold'],
|
||||
'system_leecc' => array(
|
||||
'label' => $lng['serversettings']['letsencryptecc'],
|
||||
'settinggroup' => 'system',
|
||||
'varname' => 'letsencryptreuseold',
|
||||
'type' => 'bool',
|
||||
'default' => false,
|
||||
'varname' => 'leecc',
|
||||
'type' => 'option',
|
||||
'default' => '0',
|
||||
'option_mode' => 'one',
|
||||
'option_options' => array(
|
||||
'0' => '-',
|
||||
'256' => 'ec-256',
|
||||
'384' => 'ec-384'
|
||||
),
|
||||
'save_method' => 'storeSettingField'
|
||||
),
|
||||
'system_disable_le_selfcheck' => array(
|
||||
|
||||
@@ -644,6 +644,7 @@ opcache.interned_strings_buffer'),
|
||||
('system', 'logfiles_script', ''),
|
||||
('system', 'dhparams_file', ''),
|
||||
('system', 'errorlog_level', 'warn'),
|
||||
('system', 'leecc', '0'),
|
||||
('api', 'enabled', '0'),
|
||||
('2fa', 'enabled', '1'),
|
||||
('panel', 'decimal_places', '4'),
|
||||
@@ -679,7 +680,7 @@ opcache.interned_strings_buffer'),
|
||||
('panel', 'customer_hide_options', ''),
|
||||
('panel', 'is_configured', '0'),
|
||||
('panel', 'version', '0.10.0'),
|
||||
('panel', 'db_version', '201902120');
|
||||
('panel', 'db_version', '201902170');
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS `panel_tasks`;
|
||||
|
||||
@@ -196,3 +196,24 @@ if (\Froxlor\Froxlor::isDatabaseVersion('201812190')) {
|
||||
|
||||
\Froxlor\Froxlor::updateToDbVersion('201902120');
|
||||
}
|
||||
|
||||
if (\Froxlor\Froxlor::isDatabaseVersion('201902120')) {
|
||||
|
||||
showUpdateStep("Adding new ECC / ECDSA setting for Let's Encrypt");
|
||||
Settings::AddNew('system.leecc', '0');
|
||||
Database::query("UPDATE `" . TABLE_PANEL_CRONRUNS . "` SET `cronclass` = '\\Froxlor\\Cron\\Http\\LetsEncrypt\\AcmeSh' WHERE `cronfile` = 'letsencrypt'");
|
||||
lastStepStatus(0);
|
||||
|
||||
showUpdateStep("Removing current Let's Encrypt certificates due to new implementation of acme.sh");
|
||||
$sel_result = Database::query("SELECT id FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `letsencrypt` = '1'");
|
||||
$domain_ids = $sel_result->fetchAll(\PDO::FETCH_ASSOC);
|
||||
$domain_in = "";
|
||||
foreach ($domain_ids as $domain_id) {
|
||||
$domain_in .= "'".$domain_id['id']."',";
|
||||
}
|
||||
$domain_in = substr($domain_in, 0, -1);
|
||||
Database::query("DELETE FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` IN (". $domain_in . ")");
|
||||
lastStepStatus(0);
|
||||
|
||||
\Froxlor\Froxlor::updateToDbVersion('201902170');
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php
|
||||
namespace Froxlor\Cron\Http\LetsEncrypt;
|
||||
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\FroxlorLogger;
|
||||
use Froxlor\Settings;
|
||||
use Froxlor\Database\Database;
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
@@ -22,87 +22,84 @@ use Froxlor\Settings;
|
||||
* @since 0.9.35
|
||||
*
|
||||
*/
|
||||
class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
class AcmeSh extends \Froxlor\Cron\FroxlorCron
|
||||
{
|
||||
|
||||
private static $apiserver = "";
|
||||
|
||||
private static $acmesh = "/root/.acme.sh/acme.sh";
|
||||
|
||||
public static function run()
|
||||
{
|
||||
if (Settings::Get('system.leapiversion') == '2') {
|
||||
// use ACME v2 is specified
|
||||
\Froxlor\Cron\Http\LetsEncrypt\LetsEncryptV2::run();
|
||||
exit();
|
||||
}
|
||||
self::checkInstall();
|
||||
|
||||
self::$apiserver = 'https://acme-v0' . \Froxlor\Settings::Get('system.leapiversion') . '.api.letsencrypt.org/directory';
|
||||
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating Let's Encrypt certificates");
|
||||
|
||||
if (! extension_loaded('curl')) {
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Let's Encrypt requires the php cURL extension to be installed.");
|
||||
exit();
|
||||
}
|
||||
|
||||
$certificates_stmt = Database::query("
|
||||
SELECT
|
||||
domssl.`id`,
|
||||
domssl.`domainid`,
|
||||
domssl.expirationdate,
|
||||
domssl.`ssl_cert_file`,
|
||||
domssl.`ssl_key_file`,
|
||||
domssl.`ssl_ca_file`,
|
||||
domssl.`ssl_csr_file`,
|
||||
dom.`domain`,
|
||||
dom.`wwwserveralias`,
|
||||
dom.`documentroot`,
|
||||
dom.`id` AS 'domainid',
|
||||
dom.`ssl_redirect`,
|
||||
cust.`leprivatekey`,
|
||||
cust.`lepublickey`,
|
||||
cust.`leregistered`,
|
||||
cust.`customerid`,
|
||||
cust.`loginname`
|
||||
FROM
|
||||
`" . TABLE_PANEL_CUSTOMERS . "` AS cust,
|
||||
`" . TABLE_PANEL_DOMAINS . "` AS dom
|
||||
LEFT JOIN
|
||||
`" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON
|
||||
dom.`id` = domssl.`domainid`
|
||||
WHERE
|
||||
dom.`customerid` = cust.`customerid`
|
||||
AND cust.deactivated = 0
|
||||
AND dom.`letsencrypt` = 1
|
||||
AND dom.`aliasdomain` IS NULL
|
||||
AND dom.`iswildcarddomain` = 0
|
||||
AND (
|
||||
domssl.`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||||
OR domssl.`expirationdate` IS NULL
|
||||
)
|
||||
");
|
||||
SELECT
|
||||
domssl.`id`,
|
||||
domssl.`domainid`,
|
||||
domssl.expirationdate,
|
||||
domssl.`ssl_cert_file`,
|
||||
domssl.`ssl_key_file`,
|
||||
domssl.`ssl_ca_file`,
|
||||
domssl.`ssl_csr_file`,
|
||||
dom.`domain`,
|
||||
dom.`wwwserveralias`,
|
||||
dom.`documentroot`,
|
||||
dom.`id` AS 'domainid',
|
||||
dom.`ssl_redirect`,
|
||||
cust.`leprivatekey`,
|
||||
cust.`lepublickey`,
|
||||
cust.`leregistered`,
|
||||
cust.`customerid`,
|
||||
cust.`loginname`
|
||||
FROM
|
||||
`" . TABLE_PANEL_CUSTOMERS . "` AS cust,
|
||||
`" . TABLE_PANEL_DOMAINS . "` AS dom
|
||||
LEFT JOIN
|
||||
`" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON
|
||||
dom.`id` = domssl.`domainid`
|
||||
WHERE
|
||||
dom.`customerid` = cust.`customerid`
|
||||
AND cust.deactivated = 0
|
||||
AND dom.`letsencrypt` = 1
|
||||
AND dom.`aliasdomain` IS NULL
|
||||
AND dom.`iswildcarddomain` = 0
|
||||
AND (
|
||||
domssl.`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||||
OR domssl.`expirationdate` IS NULL
|
||||
)
|
||||
");
|
||||
|
||||
$aliasdomains_stmt = Database::prepare("
|
||||
SELECT
|
||||
dom.`id` as domainid,
|
||||
dom.`domain`,
|
||||
dom.`wwwserveralias`
|
||||
FROM `" . TABLE_PANEL_DOMAINS . "` AS dom
|
||||
WHERE
|
||||
dom.`aliasdomain` = :id
|
||||
AND dom.`letsencrypt` = 1
|
||||
AND dom.`iswildcarddomain` = 0
|
||||
");
|
||||
SELECT
|
||||
dom.`id` as domainid,
|
||||
dom.`domain`,
|
||||
dom.`wwwserveralias`
|
||||
FROM `" . TABLE_PANEL_DOMAINS . "` AS dom
|
||||
WHERE
|
||||
dom.`aliasdomain` = :id
|
||||
AND dom.`letsencrypt` = 1
|
||||
AND dom.`iswildcarddomain` = 0
|
||||
");
|
||||
|
||||
$updcert_stmt = Database::prepare("
|
||||
REPLACE INTO
|
||||
`" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
|
||||
SET
|
||||
`id` = :id,
|
||||
`domainid` = :domainid,
|
||||
`ssl_cert_file` = :crt,
|
||||
`ssl_key_file` = :key,
|
||||
`ssl_ca_file` = :ca,
|
||||
`ssl_cert_chainfile` = :chain,
|
||||
`ssl_csr_file` = :csr,
|
||||
`ssl_fullchain_file` = :fullchain,
|
||||
`expirationdate` = :expirationdate
|
||||
");
|
||||
REPLACE INTO
|
||||
`" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
|
||||
SET
|
||||
`id` = :id,
|
||||
`domainid` = :domainid,
|
||||
`ssl_cert_file` = :crt,
|
||||
`ssl_key_file` = :key,
|
||||
`ssl_ca_file` = :ca,
|
||||
`ssl_cert_chainfile` = :chain,
|
||||
`ssl_csr_file` = :csr,
|
||||
`ssl_fullchain_file` = :fullchain,
|
||||
`expirationdate` = :expirationdate
|
||||
");
|
||||
|
||||
$upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid");
|
||||
|
||||
@@ -130,14 +127,15 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
);
|
||||
|
||||
$froxlor_ssl_settings_stmt = Database::prepare("
|
||||
SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
|
||||
WHERE `domainid` = '0' AND
|
||||
(`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL)
|
||||
");
|
||||
SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
|
||||
WHERE `domainid` = '0' AND
|
||||
(`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL)
|
||||
");
|
||||
$froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
|
||||
|
||||
$insert_or_update_required = true;
|
||||
$cert_mode = 'issue';
|
||||
if ($froxlor_ssl) {
|
||||
$cert_mode = 'renew';
|
||||
$certrow['id'] = $froxlor_ssl['id'];
|
||||
$certrow['expirationdate'] = $froxlor_ssl['expirationdate'];
|
||||
$certrow['ssl_cert_file'] = $froxlor_ssl['ssl_cert_file'];
|
||||
@@ -148,38 +146,51 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
// check whether we have an entry with valid certificates which just does not need
|
||||
// updating yet, so we need to skip this here
|
||||
$froxlor_ssl_settings_stmt = Database::prepare("
|
||||
SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0'
|
||||
");
|
||||
SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0'
|
||||
");
|
||||
$froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
|
||||
if ($froxlor_ssl && ! empty($froxlor_ssl['ssl_cert_file'])) {
|
||||
$insert_or_update_required = false;
|
||||
$cert_mode = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($insert_or_update_required) {
|
||||
if ($cert_mode) {
|
||||
$domains = array(
|
||||
$certrow['domain']
|
||||
);
|
||||
|
||||
// Only renew let's encrypt certificate if no broken ssl_redirect is enabled
|
||||
// - this temp. deactivation of the ssl-redirect is handled by the webserver-cronjob
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']);
|
||||
if ($cert_mode == 'renew') {
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']);
|
||||
} else {
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating certificate for " . $certrow['domain']);
|
||||
}
|
||||
|
||||
$cronlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => $certrow['loginname']
|
||||
'loginname' => $certrow['loginname'],
|
||||
'adminsession' => 0
|
||||
));
|
||||
|
||||
try {
|
||||
// Initialize Lescript with documentroot
|
||||
$le = new \Froxlor\Http\LetsEncrypt\LeScript($cronlog, \Froxlor\Froxlor::getVersion());
|
||||
// Initialize Lescript with documentroot
|
||||
$acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --" . $cert_mode . " -d " . implode(" -d ", $domains);
|
||||
|
||||
// Initialize Lescript
|
||||
$le->initAccount($certrow, true);
|
||||
if ($cert_mode == 'issue') {
|
||||
$acmesh_cmd .= " -w " . \Froxlor\Froxlor::getInstallDir();
|
||||
}
|
||||
if (Settings::Get('system.leecc') > 0) {
|
||||
$acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc');
|
||||
}
|
||||
|
||||
// Request the new certificate (old key may be used)
|
||||
$return = $le->signDomains($domains, $certrow['ssl_key_file']);
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_DEBUG, $acmesh_cmd);
|
||||
|
||||
$acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd);
|
||||
|
||||
$return = array();
|
||||
self::readCertificateToVar(array_pop($domains), $return);
|
||||
|
||||
if (! empty($return['crt'])) {
|
||||
|
||||
// We are interessted in the expirationdate
|
||||
$newcert = openssl_x509_parse($return['crt']);
|
||||
|
||||
// Store the new data
|
||||
@@ -198,28 +209,34 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
if ($certrow['ssl_redirect'] == 3) {
|
||||
Settings::Set('system.le_froxlor_redirect', '1');
|
||||
}
|
||||
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']);
|
||||
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']);
|
||||
$changedetected = 1;
|
||||
} catch (\Exception $e) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage());
|
||||
} else {
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// customer domains
|
||||
$certrows = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
$cert_mode = 'issue';
|
||||
foreach ($certrows as $certrow) {
|
||||
|
||||
// set logger to corresponding loginname for the log to appear in the users system-log
|
||||
$cronlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => $certrow['loginname']
|
||||
'loginname' => $certrow['loginname'],
|
||||
'adminsession' => 0
|
||||
));
|
||||
|
||||
// Only renew let's encrypt certificate if no broken ssl_redirect is enabled
|
||||
if ($certrow['ssl_redirect'] != 2) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']);
|
||||
|
||||
if (!empty($certrow['ssl_cert_file'])) {
|
||||
$cert_mode = 'renew';
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating certificate for " . $certrow['domain']);
|
||||
} else {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Creating certificate for " . $certrow['domain']);
|
||||
}
|
||||
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $certrow['domain']);
|
||||
$domains = array(
|
||||
@@ -245,17 +262,23 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize Lescript with documentroot
|
||||
$le = new \Froxlor\Http\LetsEncrypt\LeScript($cronlog, \Froxlor\Froxlor::getVersion());
|
||||
// Initialize Lescript with documentroot
|
||||
$acmesh_cmd = self::$acmesh . " --server " . self::$apiserver . " --" . $cert_mode . " -d " . implode(" -d ", $domains);
|
||||
|
||||
// Initialize Lescript
|
||||
$le->initAccount($certrow);
|
||||
if ($cert_mode == 'issue') {
|
||||
$acmesh_cmd .= " -w " . \Froxlor\Froxlor::getInstallDir();
|
||||
}
|
||||
if (Settings::Get('system.leecc') > 0) {
|
||||
$acmesh_cmd .= " --keylength ec-" . Settings::Get('system.leecc');
|
||||
}
|
||||
|
||||
// Request the new certificate (old key may be used)
|
||||
$return = $le->signDomains($domains, $certrow['ssl_key_file']);
|
||||
$acme_result = \Froxlor\FileDir::safe_exec($acmesh_cmd);
|
||||
|
||||
$return = array();
|
||||
self::readCertificateToVar($certrow['domain'], $return);
|
||||
|
||||
if (! empty($return['crt'])) {
|
||||
|
||||
// We are interessted in the expirationdate
|
||||
$newcert = openssl_x509_parse($return['crt']);
|
||||
|
||||
// Store the new data
|
||||
@@ -278,10 +301,9 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
}
|
||||
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']);
|
||||
|
||||
$changedetected = 1;
|
||||
} catch (\Exception $e) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage());
|
||||
} else {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ":\n" . implode("\n", $acme_result));
|
||||
}
|
||||
} else {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect");
|
||||
@@ -294,10 +316,33 @@ class LetsEncrypt extends \Froxlor\Cron\FroxlorCron
|
||||
\Froxlor\System\Cronjob::inserttask(1);
|
||||
}
|
||||
|
||||
// reset logger
|
||||
$cronlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => 'cronjob'
|
||||
));
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated");
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated");
|
||||
}
|
||||
|
||||
private static function readCertificateToVar($domain, &$return)
|
||||
{
|
||||
$certificate_folder = dirname(self::$acmesh) . "/" . $domain;
|
||||
if (Settings::Get('system.leecc') > 0) {
|
||||
$certificate_folder .= "_ecc";
|
||||
}
|
||||
$certificate_folder = \Froxlor\FileDir::makeCorrectDir($certificate_folder);
|
||||
|
||||
if (is_dir($certificate_folder)) {
|
||||
$return['crt'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.cer'));
|
||||
$return['key'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.key'));
|
||||
$return['chain'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/ca.cer'));
|
||||
$return['fullchain'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/fullchain.cer'));
|
||||
$return['csr'] = file_get_contents(\Froxlor\FileDir::makeCorrectFile($certificate_folder . '/' . $domain . '.csr'));
|
||||
}
|
||||
}
|
||||
|
||||
private static function checkInstall()
|
||||
{
|
||||
if (! file_exists(self::$acmesh)) {
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Could not find acme.sh - installing it to /root/.acme.sh/");
|
||||
\Froxlor\FileDir::safe_exec("wget -O - https://get.acme.sh | sh", false, array(
|
||||
'|'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
<?php
|
||||
namespace Froxlor\Cron\Http\LetsEncrypt;
|
||||
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\FroxlorLogger;
|
||||
use Froxlor\Settings;
|
||||
|
||||
/**
|
||||
* This file is part of the Froxlor project.
|
||||
* Copyright (c) 2016 the Froxlor Team (see authors).
|
||||
*
|
||||
* For the full copyright and license information, please view the COPYING
|
||||
* file that was distributed with this source code. You can also view the
|
||||
* COPYING file online at http://files.froxlor.org/misc/COPYING.txt
|
||||
*
|
||||
* @copyright (c) the authors
|
||||
* @author Florian Aders <kontakt-froxlor@neteraser.de>
|
||||
* @author Froxlor team <team@froxlor.org> (2016-)
|
||||
* @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
|
||||
* @package Cron
|
||||
*
|
||||
* @since 0.9.35
|
||||
*
|
||||
*/
|
||||
class LetsEncryptV2 extends \Froxlor\Cron\FroxlorCron
|
||||
{
|
||||
|
||||
public static function run()
|
||||
{
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating Let's Encrypt certificates");
|
||||
|
||||
if (! extension_loaded('curl')) {
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Let's Encrypt requires the php cURL extension to be installed.");
|
||||
exit();
|
||||
}
|
||||
|
||||
$certificates_stmt = Database::query("
|
||||
SELECT
|
||||
domssl.`id`,
|
||||
domssl.`domainid`,
|
||||
domssl.expirationdate,
|
||||
domssl.`ssl_cert_file`,
|
||||
domssl.`ssl_key_file`,
|
||||
domssl.`ssl_ca_file`,
|
||||
domssl.`ssl_csr_file`,
|
||||
dom.`domain`,
|
||||
dom.`wwwserveralias`,
|
||||
dom.`iswildcarddomain`,
|
||||
dom.`documentroot`,
|
||||
dom.`id` AS 'domainid',
|
||||
dom.`ssl_redirect`,
|
||||
cust.`leprivatekey`,
|
||||
cust.`lepublickey`,
|
||||
cust.`leregistered`,
|
||||
cust.`leaccount`,
|
||||
cust.`customerid`,
|
||||
cust.`loginname`
|
||||
FROM
|
||||
`" . TABLE_PANEL_CUSTOMERS . "` AS cust,
|
||||
`" . TABLE_PANEL_DOMAINS . "` AS dom
|
||||
LEFT JOIN
|
||||
`" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` AS domssl ON
|
||||
dom.`id` = domssl.`domainid`
|
||||
WHERE
|
||||
dom.`customerid` = cust.`customerid`
|
||||
AND cust.deactivated = 0
|
||||
AND dom.`letsencrypt` = 1
|
||||
AND dom.`aliasdomain` IS NULL
|
||||
AND dom.`iswildcarddomain` = 0
|
||||
AND (
|
||||
domssl.`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY)
|
||||
OR domssl.`expirationdate` IS NULL
|
||||
)
|
||||
");
|
||||
|
||||
$aliasdomains_stmt = Database::prepare("
|
||||
SELECT
|
||||
dom.`id` as domainid,
|
||||
dom.`domain`,
|
||||
dom.`wwwserveralias`,
|
||||
dom.`iswildcarddomain`
|
||||
FROM `" . TABLE_PANEL_DOMAINS . "` AS dom
|
||||
WHERE
|
||||
dom.`aliasdomain` = :id
|
||||
AND dom.`letsencrypt` = 1
|
||||
AND dom.`iswildcarddomain` = 0
|
||||
");
|
||||
|
||||
$updcert_stmt = Database::prepare("
|
||||
REPLACE INTO
|
||||
`" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
|
||||
SET
|
||||
`id` = :id,
|
||||
`domainid` = :domainid,
|
||||
`ssl_cert_file` = :crt,
|
||||
`ssl_key_file` = :key,
|
||||
`ssl_ca_file` = :ca,
|
||||
`ssl_cert_chainfile` = :chain,
|
||||
`ssl_csr_file` = :csr,
|
||||
`ssl_fullchain_file` = :fullchain,
|
||||
`expirationdate` = :expirationdate
|
||||
");
|
||||
|
||||
$upddom_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid");
|
||||
|
||||
// flag for re-generation of vhost files
|
||||
$changedetected = 0;
|
||||
|
||||
// first - generate LE for system-vhost if enabled
|
||||
if (Settings::Get('system.le_froxlor_enabled') == '1') {
|
||||
|
||||
$certrow = array(
|
||||
'loginname' => 'froxlor.panel',
|
||||
'domain' => Settings::Get('system.hostname'),
|
||||
'domainid' => 0,
|
||||
'documentroot' => \Froxlor\Froxlor::getInstallDir(),
|
||||
'leprivatekey' => Settings::Get('system.leprivatekey'),
|
||||
'lepublickey' => Settings::Get('system.lepublickey'),
|
||||
'leregistered' => Settings::Get('system.leregistered'),
|
||||
'leaccount' => Settings::Get('system.leaccount'),
|
||||
'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'),
|
||||
'expirationdate' => null,
|
||||
'ssl_cert_file' => null,
|
||||
'ssl_key_file' => null,
|
||||
'ssl_ca_file' => null,
|
||||
'ssl_csr_file' => null,
|
||||
'id' => null
|
||||
);
|
||||
|
||||
$froxlor_ssl_settings_stmt = Database::prepare("
|
||||
SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "`
|
||||
WHERE `domainid` = '0' AND
|
||||
(`expirationdate` < DATE_ADD(NOW(), INTERVAL 30 DAY) OR `expirationdate` IS NULL)
|
||||
");
|
||||
$froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
|
||||
|
||||
$insert_or_update_required = true;
|
||||
if ($froxlor_ssl) {
|
||||
$certrow['id'] = $froxlor_ssl['id'];
|
||||
$certrow['expirationdate'] = $froxlor_ssl['expirationdate'];
|
||||
$certrow['ssl_cert_file'] = $froxlor_ssl['ssl_cert_file'];
|
||||
$certrow['ssl_key_file'] = $froxlor_ssl['ssl_key_file'];
|
||||
$certrow['ssl_ca_file'] = $froxlor_ssl['ssl_ca_file'];
|
||||
$certrow['ssl_csr_file'] = $froxlor_ssl['ssl_csr_file'];
|
||||
} else {
|
||||
// check whether we have an entry with valid certificates which just does not need
|
||||
// updating yet, so we need to skip this here
|
||||
$froxlor_ssl_settings_stmt = Database::prepare("
|
||||
SELECT * FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` WHERE `domainid` = '0'
|
||||
");
|
||||
$froxlor_ssl = Database::pexecute_first($froxlor_ssl_settings_stmt);
|
||||
if ($froxlor_ssl && ! empty($froxlor_ssl['ssl_cert_file'])) {
|
||||
$insert_or_update_required = false;
|
||||
}
|
||||
}
|
||||
|
||||
if ($insert_or_update_required) {
|
||||
$domains = array(
|
||||
$certrow['domain']
|
||||
);
|
||||
|
||||
// Only renew let's encrypt certificate if no broken ssl_redirect is enabled
|
||||
// - this temp. deactivation of the ssl-redirect is handled by the webserver-cronjob
|
||||
FroxlorLogger::getInstanceOf()->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']);
|
||||
|
||||
$cronlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => $certrow['loginname']
|
||||
));
|
||||
|
||||
try {
|
||||
// Initialize Lescript with documentroot
|
||||
$le = new \Froxlor\Http\LetsEncrypt\LeScriptV2($cronlog, \Froxlor\Froxlor::getVersion());
|
||||
|
||||
// Initialize Lescript
|
||||
$le->initAccount($certrow, true);
|
||||
|
||||
// Request the new certificate (old key may be used)
|
||||
$return = $le->signDomains($domains, $certrow['ssl_key_file']);
|
||||
|
||||
// We are interessted in the expirationdate
|
||||
$newcert = openssl_x509_parse($return['crt']);
|
||||
|
||||
// Store the new data
|
||||
Database::pexecute($updcert_stmt, array(
|
||||
'id' => $certrow['id'],
|
||||
'domainid' => $certrow['domainid'],
|
||||
'crt' => $return['crt'],
|
||||
'key' => $return['key'],
|
||||
'ca' => $return['chain'],
|
||||
'chain' => $return['chain'],
|
||||
'csr' => $return['csr'],
|
||||
'fullchain' => $return['fullchain'],
|
||||
'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t'])
|
||||
));
|
||||
|
||||
if ($certrow['ssl_redirect'] == 3) {
|
||||
Settings::Set('system.le_froxlor_redirect', '1');
|
||||
}
|
||||
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']);
|
||||
|
||||
$changedetected = 1;
|
||||
} catch (\Exception $e) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// customer domains
|
||||
$certrows = $certificates_stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
foreach ($certrows as $certrow) {
|
||||
|
||||
// set logger to corresponding loginname for the log to appear in the users system-log
|
||||
$cronlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => $certrow['loginname']
|
||||
));
|
||||
|
||||
// Only renew let's encrypt certificate if no broken ssl_redirect is enabled
|
||||
if ($certrow['ssl_redirect'] != 2) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updating " . $certrow['domain']);
|
||||
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $certrow['domain']);
|
||||
$domains = array(
|
||||
$certrow['domain']
|
||||
);
|
||||
if ($certrow['iswildcarddomain'] == 1) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: *." . $certrow['domain']);
|
||||
$domains[] = '*.' . $certrow['domain'];
|
||||
} elseif ($certrow['wwwserveralias'] == 1) {
|
||||
// add www.<domain> to SAN list
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $certrow['domain']);
|
||||
$domains[] = 'www.' . $certrow['domain'];
|
||||
}
|
||||
|
||||
// add alias domains (and possibly www.<aliasdomain>) to SAN list
|
||||
Database::pexecute($aliasdomains_stmt, array(
|
||||
'id' => $certrow['domainid']
|
||||
));
|
||||
$aliasdomains = $aliasdomains_stmt->fetchAll(\PDO::FETCH_ASSOC);
|
||||
foreach ($aliasdomains as $aliasdomain) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: " . $aliasdomain['domain']);
|
||||
$domains[] = $aliasdomain['domain'];
|
||||
if ($aliasdomain['iswildcarddomain'] == 1) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: *." . $aliasdomain['domain']);
|
||||
$domains[] = '*.' . $aliasdomain['domain'];
|
||||
} elseif ($aliasdomain['wwwserveralias'] == 1) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Adding SAN entry: www." . $aliasdomain['domain']);
|
||||
$domains[] = 'www.' . $aliasdomain['domain'];
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// Initialize Lescript with documentroot
|
||||
$le = new \Froxlor\Http\LetsEncrypt\LeScriptV2($cronlog, \Froxlor\Froxlor::getVersion());
|
||||
|
||||
// Initialize Lescript
|
||||
$le->initAccount($certrow);
|
||||
|
||||
// Request the new certificate (old key may be used)
|
||||
$return = $le->signDomains($domains, $certrow['ssl_key_file']);
|
||||
|
||||
// We are interessted in the expirationdate
|
||||
$newcert = openssl_x509_parse($return['crt']);
|
||||
|
||||
// Store the new data
|
||||
Database::pexecute($updcert_stmt, array(
|
||||
'id' => $certrow['id'],
|
||||
'domainid' => $certrow['domainid'],
|
||||
'crt' => $return['crt'],
|
||||
'key' => $return['key'],
|
||||
'ca' => $return['chain'],
|
||||
'chain' => $return['chain'],
|
||||
'csr' => $return['csr'],
|
||||
'fullchain' => $return['fullchain'],
|
||||
'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t'])
|
||||
));
|
||||
|
||||
if ($certrow['ssl_redirect'] == 3) {
|
||||
Database::pexecute($upddom_stmt, array(
|
||||
'domainid' => $certrow['domainid']
|
||||
));
|
||||
}
|
||||
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']);
|
||||
|
||||
$changedetected = 1;
|
||||
} catch (\Exception $e) {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage());
|
||||
}
|
||||
} else {
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "Skipping Let's Encrypt generation for " . $certrow['domain'] . " due to an enabled ssl_redirect");
|
||||
}
|
||||
}
|
||||
|
||||
// If we have a change in a certificate, we need to update the webserver - configs
|
||||
// This is easiest done by just creating a new task ;)
|
||||
if ($changedetected) {
|
||||
\Froxlor\System\Cronjob::inserttask(1);
|
||||
}
|
||||
|
||||
// reset logger
|
||||
$cronlog = FroxlorLogger::getInstanceOf(array(
|
||||
'loginname' => 'cronjob'
|
||||
));
|
||||
$cronlog->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated");
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ final class Froxlor
|
||||
const VERSION = '0.10.0';
|
||||
|
||||
// Database version (YYYYMMDDC where C is a daily counter)
|
||||
const DBVERSION = '201902120';
|
||||
const DBVERSION = '201902170';
|
||||
|
||||
// Distribution branding-tag (used for Debian etc.)
|
||||
const BRANDING = '';
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
namespace Froxlor\Http\LetsEncrypt;
|
||||
|
||||
class Base64UrlSafeEncoder
|
||||
{
|
||||
|
||||
public static function encode($input)
|
||||
{
|
||||
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
|
||||
}
|
||||
|
||||
public static function decode($input)
|
||||
{
|
||||
$remainder = strlen($input) % 4;
|
||||
if ($remainder) {
|
||||
$padlen = 4 - $remainder;
|
||||
$input .= str_repeat('=', $padlen);
|
||||
}
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
<?php
|
||||
namespace Froxlor\Http\LetsEncrypt;
|
||||
|
||||
class Client
|
||||
{
|
||||
|
||||
private $lastCode;
|
||||
|
||||
private $lastHeader;
|
||||
|
||||
private $base;
|
||||
|
||||
public function __construct($base)
|
||||
{
|
||||
$this->base = $base;
|
||||
}
|
||||
|
||||
private function curl($method, $url, $data = null)
|
||||
{
|
||||
$headers = array(
|
||||
'Accept: application/json',
|
||||
'Content-Type: application/json'
|
||||
);
|
||||
$handle = curl_init();
|
||||
curl_setopt($handle, CURLOPT_URL, preg_match('~^http~', $url) ? $url : $this->base . $url);
|
||||
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($handle, CURLOPT_HEADER, true);
|
||||
|
||||
// DO NOT DO THAT!
|
||||
// curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false);
|
||||
// curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);
|
||||
|
||||
switch ($method) {
|
||||
case 'GET':
|
||||
break;
|
||||
case 'POST':
|
||||
curl_setopt($handle, CURLOPT_POST, true);
|
||||
curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
|
||||
break;
|
||||
}
|
||||
$response = curl_exec($handle);
|
||||
|
||||
if (curl_errno($handle)) {
|
||||
throw new \RuntimeException('Curl: ' . curl_error($handle));
|
||||
}
|
||||
|
||||
$header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
|
||||
|
||||
$header = substr($response, 0, $header_size);
|
||||
$body = substr($response, $header_size);
|
||||
|
||||
$this->lastHeader = $header;
|
||||
$this->lastCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);
|
||||
|
||||
$data = json_decode($body, true);
|
||||
return $data === null ? $body : $data;
|
||||
}
|
||||
|
||||
public function post($url, $data)
|
||||
{
|
||||
return $this->curl('POST', $url, $data);
|
||||
}
|
||||
|
||||
public function get($url)
|
||||
{
|
||||
return $this->curl('GET', $url);
|
||||
}
|
||||
|
||||
public function getLastNonce()
|
||||
{
|
||||
if (preg_match('~Replay\-Nonce: (.+)~i', $this->lastHeader, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
$this->curl('GET', '/directory');
|
||||
return $this->getLastNonce();
|
||||
}
|
||||
|
||||
public function getLastLocation()
|
||||
{
|
||||
if (preg_match('~Location: (.+)~i', $this->lastHeader, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getLastCode()
|
||||
{
|
||||
return $this->lastCode;
|
||||
}
|
||||
|
||||
public function getLastLinks()
|
||||
{
|
||||
preg_match_all('~Link: <(.+)>;rel="up"~', $this->lastHeader, $matches);
|
||||
return $matches[1];
|
||||
}
|
||||
}
|
||||
@@ -1,481 +0,0 @@
|
||||
<?php
|
||||
namespace Froxlor\Http\LetsEncrypt;
|
||||
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\Settings;
|
||||
|
||||
// Copyright (c) 2015, Stanislav Humplik <sh@analogic.cz>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of the <organization> nor the
|
||||
// names of its contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
// This file is copied from https://github.com/analogic/lescript
|
||||
// and modified to work without files and integrate in Froxlor
|
||||
class LeScript
|
||||
{
|
||||
|
||||
// https://letsencrypt.org/repository/
|
||||
private $logger;
|
||||
|
||||
private $client;
|
||||
|
||||
private $accountKey;
|
||||
|
||||
private $customerid;
|
||||
|
||||
private $isFroxlorVhost;
|
||||
|
||||
private $isLeProduction;
|
||||
|
||||
private $version;
|
||||
|
||||
public function __construct($logger, $version = '1')
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->version = $version;
|
||||
if (Settings::Get('system.letsencryptca') == 'production') {
|
||||
$ca = 'https://acme-v01.api.letsencrypt.org';
|
||||
} else {
|
||||
$ca = 'https://acme-staging.api.letsencrypt.org';
|
||||
}
|
||||
$this->client = new Client($ca);
|
||||
$this->log("Using '$ca' to generate certificate");
|
||||
}
|
||||
|
||||
public function initAccount($certrow, $isFroxlorVhost = false)
|
||||
{
|
||||
// Let's see if we have the private accountkey
|
||||
$this->accountKey = $certrow['leprivatekey'];
|
||||
$this->customerId = (! $isFroxlorVhost ? $certrow['customerid'] : null);
|
||||
$this->isFroxlorVhost = $isFroxlorVhost;
|
||||
$this->isLeProduction = (Settings::Get('system.letsencryptca') == 'production');
|
||||
|
||||
$leregistered = $certrow['leregistered'];
|
||||
|
||||
if (! $this->accountKey || $this->accountKey == 'unset' || ! $this->isLeProduction) {
|
||||
|
||||
// generate and save new private key for account
|
||||
// ---------------------------------------------
|
||||
|
||||
$this->log('Creating new account key');
|
||||
$keys = $this->generateKey();
|
||||
// Only store the accountkey in production, in staging always generate a new key
|
||||
if ($this->isLeProduction) {
|
||||
if ($isFroxlorVhost) {
|
||||
Settings::Set('system.lepublickey', $keys['public']);
|
||||
Settings::Set('system.leprivatekey', $keys['private']);
|
||||
Settings::Set('system.leregistered', 0); // key is not registered
|
||||
} else {
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private, `leregistered` = :registered WHERE `customerid` = :customerid;");
|
||||
Database::pexecute($upd_stmt, array(
|
||||
'public' => $keys['public'],
|
||||
'private' => $keys['private'],
|
||||
'registered' => 0,
|
||||
'customerid' => $this->customerId
|
||||
));
|
||||
}
|
||||
}
|
||||
$leregistered = 0;
|
||||
$this->accountKey = $keys['private'];
|
||||
} else {
|
||||
$this->log('Using existing account key');
|
||||
}
|
||||
|
||||
if ($leregistered == 0) { // Account not registered
|
||||
|
||||
$this->log('Starting new account registration');
|
||||
$response = $this->postNewReg();
|
||||
if ($this->client->getLastCode() == 409) {
|
||||
$this->log('The key was already registered. Using existing account.');
|
||||
} elseif ($this->client->getLastCode() == 201) {
|
||||
$this->log('New account registered.');
|
||||
} else {
|
||||
throw new \RuntimeException("Account not initialized, probably due to rate limiting. Whole response: " . json_encode($response));
|
||||
}
|
||||
$accountUrl = $this->client->getLastLocation();
|
||||
|
||||
$leregistered = 1;
|
||||
$this->setLeRegisteredState($leregistered); // Account registered
|
||||
$this->log('Lets encrypt Terms of Service accepted');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $domains
|
||||
* @param string $domainkey
|
||||
* @param string $csr
|
||||
* optional, same behavior as $reuseCsr from the original class, but we're passing the content of the csr already
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @return string[]
|
||||
*/
|
||||
public function signDomains(array $domains, $domainkey = null, $csr = null)
|
||||
{
|
||||
if (! $this->accountKey) {
|
||||
throw new \RuntimeException("Account not initialized");
|
||||
}
|
||||
|
||||
$this->log('Starting certificate generation process for domains');
|
||||
|
||||
$privateAccountKey = openssl_pkey_get_private($this->accountKey);
|
||||
$accountKeyDetails = openssl_pkey_get_details($privateAccountKey);
|
||||
|
||||
// start domains authentication
|
||||
// ----------------------------
|
||||
|
||||
foreach ($domains as $domain) {
|
||||
|
||||
// 1. getting available authentication options
|
||||
// -------------------------------------------
|
||||
|
||||
$this->log("Requesting challenge for $domain");
|
||||
|
||||
$response = $this->signedRequest("/acme/new-authz", array(
|
||||
"resource" => "new-authz",
|
||||
"identifier" => array(
|
||||
"type" => "dns",
|
||||
"value" => $domain
|
||||
)
|
||||
));
|
||||
|
||||
if ($this->client->getLastCode() == 403) {
|
||||
$this->log("Got status 403 - setting LE status to unregistered.");
|
||||
$this->setLeRegisteredState(0);
|
||||
throw new \RuntimeException("Got 'unauthorized' response - we need to re-register at next run. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
// if response is not an array but a string, it's most likely a server-error, e.g.
|
||||
// <HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>An error occurred while processing your request.
|
||||
// <p>Reference #179.d8be1402.1458059103.3613c4db</BODY></HTML>
|
||||
if (! is_array($response)) {
|
||||
throw new \RuntimeException("Invalid response from LE for domain $domain. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
if (! array_key_exists('challenges', $response)) {
|
||||
throw new \RuntimeException("No challenges received for $domain. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
// choose http-01 challenge only
|
||||
$challenge = array_reduce($response['challenges'], function ($v, $w) {
|
||||
return $v ? $v : ($w['type'] == 'http-01' ? $w : false);
|
||||
});
|
||||
|
||||
if (! $challenge) {
|
||||
throw new \RuntimeException("HTTP Challenge for $domain is not available. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
$this->log("Got challenge token for $domain");
|
||||
$location = $this->client->getLastLocation();
|
||||
|
||||
// 2. saving authentication token for web verification
|
||||
// ---------------------------------------------------
|
||||
|
||||
$directory = Settings::Get('system.letsencryptchallengepath') . '/.well-known/acme-challenge';
|
||||
$tokenPath = $directory . '/' . $challenge['token'];
|
||||
|
||||
if (! file_exists($directory) && ! @mkdir($directory, 0755, true)) {
|
||||
throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}");
|
||||
}
|
||||
|
||||
$header = array(
|
||||
// need to be in precise order!
|
||||
"e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]),
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"])
|
||||
);
|
||||
$payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true));
|
||||
|
||||
file_put_contents($tokenPath, $payload);
|
||||
chmod($tokenPath, 0644);
|
||||
|
||||
// 3. verification process itself
|
||||
// -------------------------------
|
||||
|
||||
$uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}";
|
||||
|
||||
$this->log("Token for $domain saved at $tokenPath and should be available at $uri");
|
||||
|
||||
// simple self check
|
||||
if (Settings::Get('system.disable_le_selfcheck') == '0') {
|
||||
$selfcheckpayload = \Froxlor\Http\HttpClient::urlGet($uri, false);
|
||||
if ($payload !== trim($selfcheckpayload)) {
|
||||
$errmsg = json_encode(error_get_last());
|
||||
if ($errmsg != "null") {
|
||||
$errmsg = "; PHP error: " . $errmsg;
|
||||
} else {
|
||||
$errmsg = "";
|
||||
}
|
||||
$this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "[Lets Encrypt self-check] Please check $uri - token seems to be not available. This is just a simple self-check, it might be wrong but consider using this information when Let's Encrypt fails to issue a certificate" . $errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
$this->log("Sending request to challenge");
|
||||
|
||||
// send request to challenge
|
||||
$result = $this->signedRequest($challenge['uri'], array(
|
||||
"resource" => "challenge",
|
||||
"type" => "http-01",
|
||||
"keyAuthorization" => $payload,
|
||||
"token" => $challenge['token']
|
||||
));
|
||||
|
||||
// waiting loop
|
||||
// we wait for a maximum of 30 seconds to avoid endless loops
|
||||
$count = 0;
|
||||
do {
|
||||
if (empty($result['status']) || $result['status'] == "invalid") {
|
||||
@unlink($tokenPath);
|
||||
throw new \RuntimeException("Verification ended with error: " . json_encode($result));
|
||||
}
|
||||
$ended = ! ($result['status'] === "pending");
|
||||
|
||||
if (! $ended) {
|
||||
$this->log("Verification pending, sleeping 1s");
|
||||
sleep(1);
|
||||
$count ++;
|
||||
}
|
||||
|
||||
$result = $this->client->get($location);
|
||||
} while (! $ended && $count < 30);
|
||||
|
||||
$this->log("Verification ended with status: ${result['status']}");
|
||||
@unlink($tokenPath);
|
||||
}
|
||||
|
||||
// requesting certificate
|
||||
// ----------------------
|
||||
|
||||
// generate private key for domain if not exist
|
||||
if (empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) {
|
||||
$keys = $this->generateKey();
|
||||
$domainkey = $keys['private'];
|
||||
}
|
||||
|
||||
// load domain key
|
||||
$privateDomainKey = openssl_pkey_get_private($domainkey);
|
||||
|
||||
$this->client->getLastLinks();
|
||||
|
||||
if (empty($csr)) {
|
||||
$csr = $this->generateCSR($privateDomainKey, $domains);
|
||||
}
|
||||
|
||||
// request certificates creation
|
||||
$result = $this->signedRequest("/acme/new-cert", array(
|
||||
'resource' => 'new-cert',
|
||||
'csr' => $csr
|
||||
));
|
||||
if ($this->client->getLastCode() !== 201) {
|
||||
throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result));
|
||||
}
|
||||
$location = $this->client->getLastLocation();
|
||||
|
||||
// waiting loop
|
||||
$certificates = array();
|
||||
while (1) {
|
||||
$this->client->getLastLinks();
|
||||
|
||||
$result = $this->client->get($location);
|
||||
|
||||
if ($this->client->getLastCode() == 202) {
|
||||
|
||||
$this->log("Certificate generation pending, sleeping 1s");
|
||||
sleep(1);
|
||||
} elseif ($this->client->getLastCode() == 200) {
|
||||
|
||||
$this->log("Got certificate! YAY!");
|
||||
$certificates[] = $this->parsePemFromBody($result);
|
||||
|
||||
foreach ($this->client->getLastLinks() as $link) {
|
||||
$this->log("Requesting chained cert at $link");
|
||||
$result = $this->client->get($link);
|
||||
$certificates[] = $this->parsePemFromBody($result);
|
||||
}
|
||||
|
||||
break;
|
||||
} else {
|
||||
|
||||
throw new \RuntimeException("Can't get certificate: HTTP code " . $this->client->getLastCode());
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($certificates))
|
||||
throw new \RuntimeException('No certificates generated');
|
||||
|
||||
$fullchain = implode("\n", $certificates);
|
||||
$crt = array_shift($certificates);
|
||||
$chain = implode("\n", $certificates);
|
||||
|
||||
$this->log("Done, returning new certificates and key");
|
||||
return array(
|
||||
'fullchain' => $fullchain,
|
||||
'crt' => $crt,
|
||||
'chain' => $chain,
|
||||
'key' => $domainkey,
|
||||
'csr' => $csr
|
||||
);
|
||||
}
|
||||
|
||||
private function setLeRegisteredState($state)
|
||||
{
|
||||
if ($this->isLeProduction) {
|
||||
if ($this->isFroxlorVhost) {
|
||||
Settings::Set('system.leregistered', $state);
|
||||
} else {
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `leregistered` = :registered WHERE `customerid` = :customerid;");
|
||||
Database::pexecute($upd_stmt, array(
|
||||
'registered' => $state,
|
||||
'customerid' => $this->customerId
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parsePemFromBody($body)
|
||||
{
|
||||
$pem = chunk_split(base64_encode($body), 64, "\n");
|
||||
return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
|
||||
}
|
||||
|
||||
private function postNewReg()
|
||||
{
|
||||
$this->log('Getting last terms of service URL');
|
||||
$directory = $this->client->get('/directory');
|
||||
if (! isset($directory['meta']) || ! isset($directory['meta']['terms-of-service'])) {
|
||||
throw new \RuntimeException("No terms of service link available!");
|
||||
}
|
||||
$this->log('Sending registration to letsencrypt server');
|
||||
|
||||
return $this->signedRequest('/acme/new-reg', array(
|
||||
'resource' => 'new-reg',
|
||||
'agreement' => $directory['meta']['terms-of-service']
|
||||
));
|
||||
}
|
||||
|
||||
private function generateCSR($privateKey, array $domains)
|
||||
{
|
||||
$domain = reset($domains);
|
||||
$san = implode(",", array_map(function ($dns) {
|
||||
return "DNS:" . $dns;
|
||||
}, $domains));
|
||||
$tmpConf = tmpfile();
|
||||
$tmpConfMeta = stream_get_meta_data($tmpConf);
|
||||
$tmpConfPath = $tmpConfMeta["uri"];
|
||||
|
||||
// workaround to get SAN working
|
||||
fwrite($tmpConf, 'HOME = .
|
||||
RANDFILE = $ENV::HOME/.rnd
|
||||
[ req ]
|
||||
default_bits = ' . Settings::Get('system.letsencryptkeysize') . '
|
||||
default_keyfile = privkey.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
[ req_distinguished_name ]
|
||||
countryName = Country Name (2 letter code)
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
subjectAltName = ' . $san . '
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment');
|
||||
|
||||
$csr = openssl_csr_new(array(
|
||||
"CN" => $domain,
|
||||
"ST" => Settings::Get('system.letsencryptstate'),
|
||||
"C" => Settings::Get('system.letsencryptcountrycode'),
|
||||
"O" => "Unknown"
|
||||
), $privateKey, array(
|
||||
"config" => $tmpConfPath,
|
||||
"digest_alg" => "sha256"
|
||||
));
|
||||
|
||||
if (! $csr)
|
||||
throw new \RuntimeException("CSR couldn't be generated! " . openssl_error_string());
|
||||
|
||||
openssl_csr_export($csr, $csr);
|
||||
fclose($tmpConf);
|
||||
|
||||
preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches);
|
||||
|
||||
return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1])));
|
||||
}
|
||||
|
||||
private function generateKey()
|
||||
{
|
||||
$res = openssl_pkey_new(array(
|
||||
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
||||
"private_key_bits" => (int) Settings::Get('system.letsencryptkeysize')
|
||||
));
|
||||
|
||||
if (! openssl_pkey_export($res, $privateKey)) {
|
||||
throw new \RuntimeException("Key export failed!");
|
||||
}
|
||||
|
||||
$details = openssl_pkey_get_details($res);
|
||||
|
||||
return array(
|
||||
'private' => $privateKey,
|
||||
'public' => $details['key']
|
||||
);
|
||||
}
|
||||
|
||||
private function signedRequest($uri, array $payload)
|
||||
{
|
||||
$privateKey = openssl_pkey_get_private($this->accountKey);
|
||||
$details = openssl_pkey_get_details($privateKey);
|
||||
|
||||
$header = array(
|
||||
"alg" => "RS256",
|
||||
"jwk" => array(
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]),
|
||||
"e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"])
|
||||
)
|
||||
);
|
||||
|
||||
$protected = $header;
|
||||
$protected["nonce"] = $this->client->getLastNonce();
|
||||
|
||||
$payload64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload)));
|
||||
$protected64 = Base64UrlSafeEncoder::encode(json_encode($protected));
|
||||
|
||||
openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256");
|
||||
|
||||
$signed64 = Base64UrlSafeEncoder::encode($signed);
|
||||
|
||||
$data = array(
|
||||
'header' => $header,
|
||||
'protected' => $protected64,
|
||||
'payload' => $payload64,
|
||||
'signature' => $signed64
|
||||
);
|
||||
|
||||
$this->log("Sending signed request to $uri");
|
||||
|
||||
return $this->client->post($uri, json_encode($data));
|
||||
}
|
||||
|
||||
protected function log($message)
|
||||
{
|
||||
$this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "letsencrypt " . $message);
|
||||
}
|
||||
}
|
||||
@@ -1,498 +0,0 @@
|
||||
<?php
|
||||
namespace Froxlor\Http\LetsEncrypt;
|
||||
|
||||
use Froxlor\Database\Database;
|
||||
use Froxlor\Settings;
|
||||
|
||||
// Copyright (c) 2015, Stanislav Humplik <sh@analogic.cz>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are met:
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above copyright
|
||||
// notice, this list of conditions and the following disclaimer in the
|
||||
// documentation and/or other materials provided with the distribution.
|
||||
// * Neither the name of the <organization> nor the
|
||||
// names of its contributors may be used to endorse or promote products
|
||||
// derived from this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
// This file is copied from https://github.com/analogic/lescript
|
||||
// and modified to work without files and integrate in Froxlor
|
||||
class LeScriptV2
|
||||
{
|
||||
|
||||
// https://letsencrypt.org/repository/
|
||||
private $logger;
|
||||
|
||||
private $client;
|
||||
|
||||
private $accountKey;
|
||||
|
||||
private $customerid;
|
||||
|
||||
private $isFroxlorVhost;
|
||||
|
||||
private $isLeProduction;
|
||||
|
||||
private $version;
|
||||
|
||||
private $req_uris = array();
|
||||
|
||||
private $acc_location = null;
|
||||
|
||||
public function __construct($logger, $version = '2')
|
||||
{
|
||||
$this->logger = $logger;
|
||||
$this->version = $version;
|
||||
if (Settings::Get('system.letsencryptca') == 'production') {
|
||||
$ca = 'https://acme-v02.api.letsencrypt.org';
|
||||
} else {
|
||||
$ca = 'https://acme-staging-v02.api.letsencrypt.org';
|
||||
}
|
||||
$this->client = new Client($ca);
|
||||
$this->log("Using '$ca' to generate certificate");
|
||||
|
||||
// get request-uris from /directory
|
||||
$response = $this->client->get('/directory');
|
||||
$this->req_uris['newAccount'] = $response['newAccount'];
|
||||
$this->req_uris['newOrder'] = $response['newOrder'];
|
||||
$this->req_uris['newNonce'] = $response['newNonce'];
|
||||
$this->req_uris['revokeCert'] = $response['revokeCert'];
|
||||
}
|
||||
|
||||
public function initAccount($certrow, $isFroxlorVhost = false)
|
||||
{
|
||||
// Let's see if we have the private accountkey
|
||||
$this->accountKey = $certrow['leprivatekey'];
|
||||
$this->customerId = (! $isFroxlorVhost ? $certrow['customerid'] : null);
|
||||
$this->isFroxlorVhost = $isFroxlorVhost;
|
||||
$this->isLeProduction = (Settings::Get('system.letsencryptca') == 'production');
|
||||
$this->acc_location = $certrow['leaccount'];
|
||||
|
||||
$leregistered = $certrow['leregistered'];
|
||||
|
||||
if (! $this->accountKey || $this->accountKey == 'unset' || ! $this->isLeProduction) {
|
||||
|
||||
// generate and save new private key for account
|
||||
// ---------------------------------------------
|
||||
|
||||
$this->log('Creating new account key');
|
||||
$keys = $this->generateKey();
|
||||
// Only store the accountkey in production, in staging always generate a new key
|
||||
if ($this->isLeProduction) {
|
||||
if ($isFroxlorVhost) {
|
||||
Settings::Set('system.lepublickey', $keys['public']);
|
||||
Settings::Set('system.leprivatekey', $keys['private']);
|
||||
Settings::Set('system.leregistered', 0); // key is not registered
|
||||
} else {
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private, `leregistered` = :registered WHERE `customerid` = :customerid;");
|
||||
Database::pexecute($upd_stmt, array(
|
||||
'public' => $keys['public'],
|
||||
'private' => $keys['private'],
|
||||
'registered' => 0,
|
||||
'customerid' => $this->customerId
|
||||
));
|
||||
}
|
||||
}
|
||||
$leregistered = 0;
|
||||
$this->accountKey = $keys['private'];
|
||||
} else {
|
||||
$this->log('Using existing account key');
|
||||
}
|
||||
|
||||
if ($leregistered == 0) { // Account not registered
|
||||
|
||||
$this->log('Starting new account registration');
|
||||
$response = $this->postNewReg();
|
||||
if ($this->client->getLastCode() == 409) {
|
||||
$this->log('The key was already registered. Using existing account.');
|
||||
} elseif ($this->client->getLastCode() == 201) {
|
||||
$this->log('New account registered.');
|
||||
} else {
|
||||
throw new \RuntimeException("Account not initialized, probably due to rate limiting. Whole response: " . json_encode($response));
|
||||
}
|
||||
$this->acc_location = $this->client->getLastLocation();
|
||||
|
||||
$leregistered = 1;
|
||||
$this->setLeRegisteredState($leregistered);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $domains
|
||||
* @param string $domainkey
|
||||
* @param string $csr
|
||||
* optional, same behavior as $reuseCsr from the original class, but we're passing the content of the csr already
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* @return string[]
|
||||
*/
|
||||
public function signDomains(array $domains, $domainkey = null, $csr = null)
|
||||
{
|
||||
if (! $this->accountKey) {
|
||||
throw new \RuntimeException("Account not initialized");
|
||||
}
|
||||
|
||||
$this->log('Starting certificate generation process for domains');
|
||||
|
||||
$privateAccountKey = openssl_pkey_get_private($this->accountKey);
|
||||
$accountKeyDetails = openssl_pkey_get_details($privateAccountKey);
|
||||
|
||||
// start domains authentication
|
||||
// ----------------------------
|
||||
|
||||
// Prepare order
|
||||
$domains_in_order = array();
|
||||
foreach ($domains as $domain) {
|
||||
$domains_in_order[] = array(
|
||||
"type" => "dns",
|
||||
"value" => $domain
|
||||
);
|
||||
}
|
||||
|
||||
// Send new-order request
|
||||
$response = $this->signedRequest($this->req_uris['newOrder'], array(
|
||||
"identifiers" => $domains_in_order
|
||||
), false);
|
||||
|
||||
if ($this->client->getLastCode() == 403) {
|
||||
$this->log("Got status 403 - setting LE status to unregistered.");
|
||||
$this->acc_location = '';
|
||||
$this->setLeRegisteredState(0);
|
||||
throw new \RuntimeException("Got 'unauthorized' response - we need to re-register at next run. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
// if response is not an array but a string, it's most likely a server-error, e.g.
|
||||
// <HTML><HEAD><TITLE>Error</TITLE></HEAD><BODY>An error occurred while processing your request.
|
||||
// <p>Reference #179.d8be1402.1458059103.3613c4db</BODY></HTML>
|
||||
if (! is_array($response)) {
|
||||
throw new \RuntimeException("Invalid response from LE for domain $domain. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
if (! array_key_exists('authorizations', $response)) {
|
||||
throw new \RuntimeException("No authorizations received for $domain. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
$authorizations = $response['authorizations'];
|
||||
$finalizeLink = $response['finalize'];
|
||||
|
||||
$i = 0;
|
||||
|
||||
foreach ($authorizations as $authorization) {
|
||||
|
||||
// 1. getting available authentication options
|
||||
// -------------------------------------------
|
||||
|
||||
$domain = $response['identifiers'][$i ++]['value'];
|
||||
|
||||
$this->log("Requesting challenge for $domain");
|
||||
|
||||
// get authorization
|
||||
$auth_response = $this->client->get($authorization);
|
||||
|
||||
if (! array_key_exists('challenges', $auth_response)) {
|
||||
throw new \RuntimeException("No challenges received for $domain. Whole response: " . json_encode($auth_response));
|
||||
}
|
||||
|
||||
// choose http-01 challenge only
|
||||
$challenge = array_reduce($auth_response['challenges'], function ($v, $w) {
|
||||
return $v ? $v : ($w['type'] == 'http-01' ? $w : false);
|
||||
});
|
||||
|
||||
if (! $challenge) {
|
||||
throw new \RuntimeException("HTTP Challenge for $domain is not available. Whole response: " . json_encode($response));
|
||||
}
|
||||
|
||||
$this->log("Got challenge token for $domain");
|
||||
$location = $challenge['url'];
|
||||
|
||||
// 2. saving authentication token for web verification
|
||||
// ---------------------------------------------------
|
||||
|
||||
$directory = Settings::Get('system.letsencryptchallengepath') . '/.well-known/acme-challenge';
|
||||
$tokenPath = $directory . '/' . $challenge['token'];
|
||||
|
||||
if (! file_exists($directory) && ! @mkdir($directory, 0755, true)) {
|
||||
throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}");
|
||||
}
|
||||
|
||||
$header = array(
|
||||
// need to be in precise order!
|
||||
"e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]),
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"])
|
||||
);
|
||||
$payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true));
|
||||
|
||||
file_put_contents($tokenPath, $payload);
|
||||
chmod($tokenPath, 0644);
|
||||
|
||||
// 3. verification process itself
|
||||
// -------------------------------
|
||||
|
||||
$uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}";
|
||||
|
||||
$this->log("Token for $domain saved at $tokenPath and should be available at $uri");
|
||||
|
||||
// simple self check
|
||||
if (Settings::Get('system.disable_le_selfcheck') == '0') {
|
||||
$selfcheckpayload = \Froxlor\Http\HttpClient::urlGet($uri, false);
|
||||
if ($payload !== trim($selfcheckpayload)) {
|
||||
$errmsg = json_encode(error_get_last());
|
||||
if ($errmsg != "null") {
|
||||
$errmsg = "; PHP error: " . $errmsg;
|
||||
} else {
|
||||
$errmsg = "";
|
||||
}
|
||||
$this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_WARNING, "[Lets Encrypt self-check] Please check $uri - token seems to be not available. This is just a simple self-check, it might be wrong but consider using this information when Let's Encrypt fails to issue a certificate" . $errmsg);
|
||||
}
|
||||
}
|
||||
|
||||
$this->log("Sending request to challenge");
|
||||
|
||||
// send request to challenge
|
||||
$result = $this->signedRequest($challenge['url'], array(
|
||||
"type" => "http-01",
|
||||
"keyAuthorization" => $payload,
|
||||
"token" => $challenge['token']
|
||||
), false);
|
||||
|
||||
// waiting loop
|
||||
// we wait for a maximum of 30 seconds to avoid endless loops
|
||||
$count = 0;
|
||||
do {
|
||||
if (empty($result['status']) || $result['status'] == "invalid") {
|
||||
@unlink($tokenPath);
|
||||
throw new \RuntimeException("Verification ended with error: " . json_encode($result));
|
||||
}
|
||||
$ended = ! ($result['status'] === "pending" || $result['status'] === "processing");
|
||||
|
||||
if (! $ended) {
|
||||
$this->log("Verification " . $result['status'] . ", sleeping 1s");
|
||||
sleep(1);
|
||||
$count ++;
|
||||
}
|
||||
|
||||
$result = $this->client->get($location);
|
||||
} while (! $ended && $count < 30);
|
||||
|
||||
$this->log("Verification ended with status: ${result['status']}");
|
||||
@unlink($tokenPath);
|
||||
}
|
||||
|
||||
// requesting certificate
|
||||
// ----------------------
|
||||
|
||||
// generate private key for domain if not exist
|
||||
if (empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) {
|
||||
$keys = $this->generateKey();
|
||||
$domainkey = $keys['private'];
|
||||
}
|
||||
|
||||
// load domain key
|
||||
$privateDomainKey = openssl_pkey_get_private($domainkey);
|
||||
|
||||
if (empty($csr)) {
|
||||
$csr = $this->generateCSR($privateDomainKey, $domains);
|
||||
}
|
||||
|
||||
// request certificates creation
|
||||
$result = $this->signedRequest($finalizeLink, array(
|
||||
'csr' => $csr
|
||||
), false);
|
||||
if ($this->client->getLastCode() !== 200) {
|
||||
throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result));
|
||||
}
|
||||
if (! isset($result['certificate'])) {
|
||||
throw new \RuntimeException("No certificate URL specified in result");
|
||||
}
|
||||
|
||||
$certificates = array();
|
||||
$certdata = $this->client->get($result['certificate']);
|
||||
$this->log("Got certificate! YAY!");
|
||||
$certificates[] = $certdata;
|
||||
foreach ($this->client->getLastLinks() as $link) {
|
||||
$this->log("Requesting chained cert at $link");
|
||||
$result = $this->client->get($link);
|
||||
$certificates[] = $result;
|
||||
}
|
||||
|
||||
if (empty($certificates))
|
||||
throw new \RuntimeException('No certificates generated');
|
||||
|
||||
$fullchain = implode("\n", $certificates);
|
||||
$crt = array_shift($certificates);
|
||||
$chain = implode("\n", $certificates);
|
||||
|
||||
$this->log("Done, returning new certificates and key");
|
||||
return array(
|
||||
'fullchain' => $fullchain,
|
||||
'crt' => $crt,
|
||||
'chain' => $chain,
|
||||
'key' => $domainkey,
|
||||
'csr' => $csr
|
||||
);
|
||||
}
|
||||
|
||||
private function setLeRegisteredState($state)
|
||||
{
|
||||
if ($this->isLeProduction) {
|
||||
if ($this->isFroxlorVhost) {
|
||||
Settings::Set('system.leregistered', $state);
|
||||
Settings::Set('system.leaccount', $this->acc_location);
|
||||
} else {
|
||||
$upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `leregistered` = :registered, `leaccount` = :kid WHERE `customerid` = :customerid;");
|
||||
Database::pexecute($upd_stmt, array(
|
||||
'registered' => $state,
|
||||
'kid' => $this->acc_location,
|
||||
'customerid' => $this->customerId
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function parsePemFromBody($body)
|
||||
{
|
||||
$pem = chunk_split(base64_encode($body), 64, "\n");
|
||||
return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n";
|
||||
}
|
||||
|
||||
private function postNewReg()
|
||||
{
|
||||
$this->log('Getting last terms of service URL');
|
||||
$directory = $this->client->get('/directory');
|
||||
if (! isset($directory['meta']) || ! isset($directory['meta']['termsOfService'])) {
|
||||
throw new \RuntimeException("No terms of service link available!");
|
||||
}
|
||||
$this->log('Sending registration to letsencrypt server');
|
||||
|
||||
return $this->signedRequest($this->req_uris['newAccount'], array(
|
||||
'termsOfServiceAgreed' => true
|
||||
));
|
||||
}
|
||||
|
||||
private function generateCSR($privateKey, array $domains)
|
||||
{
|
||||
$domain = reset($domains);
|
||||
$san = implode(",", array_map(function ($dns) {
|
||||
return "DNS:" . $dns;
|
||||
}, $domains));
|
||||
$tmpConf = tmpfile();
|
||||
$tmpConfMeta = stream_get_meta_data($tmpConf);
|
||||
$tmpConfPath = $tmpConfMeta["uri"];
|
||||
|
||||
// workaround to get SAN working
|
||||
fwrite($tmpConf, 'HOME = .
|
||||
RANDFILE = $ENV::HOME/.rnd
|
||||
[ req ]
|
||||
default_bits = ' . Settings::Get('system.letsencryptkeysize') . '
|
||||
default_keyfile = privkey.pem
|
||||
distinguished_name = req_distinguished_name
|
||||
req_extensions = v3_req
|
||||
[ req_distinguished_name ]
|
||||
countryName = Country Name (2 letter code)
|
||||
[ v3_req ]
|
||||
basicConstraints = CA:FALSE
|
||||
subjectAltName = ' . $san . '
|
||||
keyUsage = nonRepudiation, digitalSignature, keyEncipherment');
|
||||
|
||||
$csr = openssl_csr_new(array(
|
||||
"CN" => $domain,
|
||||
"ST" => Settings::Get('system.letsencryptstate'),
|
||||
"C" => Settings::Get('system.letsencryptcountrycode'),
|
||||
"O" => "Unknown"
|
||||
), $privateKey, array(
|
||||
"config" => $tmpConfPath,
|
||||
"digest_alg" => "sha256"
|
||||
));
|
||||
|
||||
if (! $csr)
|
||||
throw new \RuntimeException("CSR couldn't be generated! " . openssl_error_string());
|
||||
|
||||
openssl_csr_export($csr, $csr);
|
||||
fclose($tmpConf);
|
||||
|
||||
preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches);
|
||||
|
||||
return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1])));
|
||||
}
|
||||
|
||||
private function generateKey()
|
||||
{
|
||||
$res = openssl_pkey_new(array(
|
||||
"private_key_type" => OPENSSL_KEYTYPE_RSA,
|
||||
"private_key_bits" => (int) Settings::Get('system.letsencryptkeysize')
|
||||
));
|
||||
|
||||
if (! openssl_pkey_export($res, $privateKey)) {
|
||||
throw new \RuntimeException("Key export failed!");
|
||||
}
|
||||
|
||||
$details = openssl_pkey_get_details($res);
|
||||
|
||||
return array(
|
||||
'private' => $privateKey,
|
||||
'public' => $details['key']
|
||||
);
|
||||
}
|
||||
|
||||
private function signedRequest($uri, array $payload, $needs_jwk = true)
|
||||
{
|
||||
$privateKey = openssl_pkey_get_private($this->accountKey);
|
||||
$details = openssl_pkey_get_details($privateKey);
|
||||
|
||||
$header = array(
|
||||
"alg" => "RS256"
|
||||
);
|
||||
|
||||
if ($needs_jwk) {
|
||||
$header["jwk"] = array(
|
||||
"kty" => "RSA",
|
||||
"n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]),
|
||||
"e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"])
|
||||
);
|
||||
} else {
|
||||
// need account-url
|
||||
$header["kid"] = $this->acc_location;
|
||||
}
|
||||
|
||||
$protected = $header;
|
||||
$protected["nonce"] = $this->client->getLastNonce();
|
||||
$protected["url"] = $uri;
|
||||
|
||||
$payload64 = Base64UrlSafeEncoder::encode(json_encode($payload, JSON_UNESCAPED_SLASHES));
|
||||
$protected64 = Base64UrlSafeEncoder::encode(json_encode($protected));
|
||||
|
||||
openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256");
|
||||
|
||||
$signed64 = Base64UrlSafeEncoder::encode($signed);
|
||||
|
||||
$data = array(
|
||||
'protected' => $protected64,
|
||||
'payload' => $payload64,
|
||||
'signature' => $signed64
|
||||
);
|
||||
|
||||
$this->log("Sending signed request to $uri");
|
||||
return $this->client->post($uri, json_encode($data));
|
||||
}
|
||||
|
||||
protected function log($message)
|
||||
{
|
||||
$this->logger->logAction(\Froxlor\FroxlorLogger::CRON_ACTION, LOG_INFO, "letsencrypt-v2 " . $message);
|
||||
}
|
||||
}
|
||||
@@ -1833,17 +1833,15 @@ $lng['error']['nowildcardwithletsencrypt'] = 'Let\'s Encrypt cannot handle wildc
|
||||
$lng['panel']['letsencrypt'] = 'Using Let\'s encrypt';
|
||||
$lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates';
|
||||
$lng['serversettings']['letsencryptca']['title'] = "Let's Encrypt environment";
|
||||
$lng['serversettings']['letsencryptca']['description'] = "Environment to be used for Let's Encrypt certificates.<br><strong class=\"red\">ATTENTION:</strong> Let's Encrypt is still in beta</strong>";
|
||||
$lng['serversettings']['letsencryptca']['description'] = "Environment to be used for Let's Encrypt certificates.";
|
||||
$lng['serversettings']['letsencryptcountrycode']['title'] = "Let's Encrypt country code";
|
||||
$lng['serversettings']['letsencryptcountrycode']['description'] = "2 letter country code used to generate Let's Encrypt certificates.<br><strong class=\"red\">ATTENTION:</strong> Let's Encrypt is still in beta</strong>";
|
||||
$lng['serversettings']['letsencryptcountrycode']['description'] = "2 letter country code used to generate Let's Encrypt certificates.";
|
||||
$lng['serversettings']['letsencryptstate']['title'] = "Let's Encrypt state";
|
||||
$lng['serversettings']['letsencryptstate']['description'] = "State used to generate Let's Encrypt certificates.<br><strong class=\"red\">ATTENTION:</strong> Let's Encrypt is still in beta</strong>";
|
||||
$lng['serversettings']['letsencryptstate']['description'] = "State used to generate Let's Encrypt certificates.";
|
||||
$lng['serversettings']['letsencryptchallengepath']['title'] = "Path for Let's Encrypt challenges";
|
||||
$lng['serversettings']['letsencryptchallengepath']['description'] = "Directory where the Let's Encrypt challenges should be offered from via a global alias.<br><strong class=\"red\">ATTENTION:</strong> Let's Encrypt is still in beta</strong>";
|
||||
$lng['serversettings']['letsencryptchallengepath']['description'] = "Directory where the Let's Encrypt challenges should be offered from via a global alias.";
|
||||
$lng['serversettings']['letsencryptkeysize']['title'] = "Key size for new Let's Encrypt certificates";
|
||||
$lng['serversettings']['letsencryptkeysize']['description'] = "Size of the key in Bits for new Let's Encrypt certificates.<br><strong class=\"red\">ATTENTION:</strong> Let's Encrypt is still in beta</strong>";
|
||||
$lng['serversettings']['letsencryptreuseold']['title'] = "Re-use Let's Encrypt key";
|
||||
$lng['serversettings']['letsencryptreuseold']['description'] = "If activated, the same key will be used for every renew, otherwise a new key will be generated every time.<br><strong class=\"red\">ATTENTION:</strong> Let's Encrypt is still in beta</strong>";
|
||||
$lng['serversettings']['letsencryptkeysize']['description'] = "Size of the key in Bits for new Let's Encrypt certificates.";
|
||||
$lng['serversettings']['leenabled']['title'] = "Enable Let's Encrypt";
|
||||
$lng['serversettings']['leenabled']['description'] = "If activated, customers are able to let froxlor automatically generate and renew Let's Encrypt ssl-certificates for domains with a ssl IP/port.<br /><br />Please remember that you need to go through the webserver-configuration when enabled because this feature needs a special configuration.";
|
||||
$lng['domains']['ssl_redirect_temporarilydisabled'] = "<br>The SSL redirect is temporarily deactivated while a new Let's Encrypt certificate is generated. It will be activated again after the certificate was generated.";
|
||||
@@ -2054,3 +2052,5 @@ $lng['tasks']['remove_pdns_domain'] = 'Delete domain %s from PowerDNS database';
|
||||
$lng['admin']['novhostcontainer'] = '<br><br><small class="red">None of the IPs and ports has the "' . $lng['admin']['ipsandports']['create_vhostcontainer'] . '" option enabled, many settings here will not be available</small>';
|
||||
$lng['serversettings']['errorlog_level']['title'] = 'Error log-level';
|
||||
$lng['serversettings']['errorlog_level']['description'] = 'Specify the error log level. Default is "warn" for apache-users and "error" for nginx-users.';
|
||||
$lng['serversettings']['letsencryptecc']['title'] = "Issue ECC / ECDSA certificate";
|
||||
$lng['serversettings']['letsencryptecc']['description'] = "If set to a valid key-size the certificate issued will use ECC / ECDSA";
|
||||
|
||||
@@ -1485,17 +1485,15 @@ $lng['error']['nowildcardwithletsencrypt'] = 'Let\'s Encrypt kann in ACME v1 nic
|
||||
$lng['panel']['letsencrypt'] = 'Benutzt Let\'s encrypt';
|
||||
$lng['crondesc']['cron_letsencrypt'] = 'Aktualisierung der Let\'s Encrypt Zertifikate';
|
||||
$lng['serversettings']['letsencryptca']['title'] = "Let's Encrypt Umgebung";
|
||||
$lng['serversettings']['letsencryptca']['description'] = "Let's Encrypt - Umgebung, welche genutzt wird um Zertifikate zu bestellen.<br><strong class=\"red\">ACHTUNG:</strong> Let's Encrypt befindet sich noch im Test";
|
||||
$lng['serversettings']['letsencryptca']['description'] = "Let's Encrypt - Umgebung, welche genutzt wird um Zertifikate zu bestellen.";
|
||||
$lng['serversettings']['letsencryptcountrycode']['title'] = "Let's Encrypt Ländercode";
|
||||
$lng['serversettings']['letsencryptcountrycode']['description'] = "2 - stelliger Ländercode, welcher benutzt wird um Let's Encrypt - Zertifikate zu bestellen.<br><strong class=\"red\">ACHTUNG:</strong> Let's Encrypt befindet sich noch im Test";
|
||||
$lng['serversettings']['letsencryptcountrycode']['description'] = "2 - stelliger Ländercode, welcher benutzt wird um Let's Encrypt - Zertifikate zu bestellen.";
|
||||
$lng['serversettings']['letsencryptstate']['title'] = "Let's Encrypt Bundesland";
|
||||
$lng['serversettings']['letsencryptstate']['description'] = "Bundesland, welches benutzt wird, um Let's Encrypt - Zertifikate zu bestellen.<br><strong class=\"red\">ACHTUNG:</strong> Let's Encrypt befindet sich noch im Test";
|
||||
$lng['serversettings']['letsencryptstate']['description'] = "Bundesland, welches benutzt wird, um Let's Encrypt - Zertifikate zu bestellen.";
|
||||
$lng['serversettings']['letsencryptchallengepath']['title'] = "Verzeichnis für Let's Encrypt challenges";
|
||||
$lng['serversettings']['letsencryptchallengepath']['description'] = "Let's Encrypt challenges werden aus diesem Verzeichnis über einen globalen Alias ausgeliefert.<br><strong class=\"red\">ACHTUNG:</strong> Let's Encrypt befindet sich noch im Test";
|
||||
$lng['serversettings']['letsencryptchallengepath']['description'] = "Let's Encrypt challenges werden aus diesem Verzeichnis über einen globalen Alias ausgeliefert.";
|
||||
$lng['serversettings']['letsencryptkeysize']['title'] = "Schlüsselgröße für neue Let's Encrypt Zertifikate";
|
||||
$lng['serversettings']['letsencryptkeysize']['description'] = "Größe des Schlüssels in Bit für neue Let's Encrypt Zertifikate.<br><strong class=\"red\">ACHTUNG:</strong> Let's Encrypt befindet sich noch im Test";
|
||||
$lng['serversettings']['letsencryptreuseold']['title'] = "Let's Encrypt Schlüssel wiederverwenden";
|
||||
$lng['serversettings']['letsencryptreuseold']['description'] = "Wenn dies aktiviert ist, wird der alte Schlüssel bei jeder Verlängerung verwendet, andernfalls wird ein neues Paar generiert.<br><strong class=\"red\">ACHTUNG:</strong> Let's Encrypt befindet sich noch im Test";
|
||||
$lng['serversettings']['letsencryptkeysize']['description'] = "Größe des Schlüssels in Bit für neue Let's Encrypt Zertifikate.";
|
||||
$lng['serversettings']['leenabled']['title'] = "Let's Encrypt verwenden";
|
||||
$lng['serversettings']['leenabled']['description'] = "Wenn dies aktiviert ist, können Kunden durch Froxlor automatisch generierte und verlängerbare Let's Encrypt SSL-Zertifikate für Domains mit SSL IP/Port nutzen.<br /><br />Bitte die Webserver-Konfiguration beachten wenn aktiviert, da dieses Feature eine spezielle Konfiguration benötigt.";
|
||||
$lng['domains']['ssl_redirect_temporarilydisabled'] = "<br>Die SSL-Umleitung ist, während ein neues Let's Encrypt - Zertifikat erstellt wird, temporär deaktiviert. Die Umleitung wird nach der Zertifikatserstellung wieder aktiviert.";
|
||||
@@ -1701,3 +1699,5 @@ $lng['tasks']['remove_pdns_domain'] = 'Lösche Domain %s von PowerDNS Datenbank'
|
||||
$lng['admin']['novhostcontainer'] = '<br><br><small class="red">Keine der IPs und Ports hat die Option "' . $lng['admin']['ipsandports']['create_vhostcontainer'] . '" aktiviert, einige Einstellungen sind daher nicht verfügbar.</small>';
|
||||
$lng['serversettings']['errorlog_level']['title'] = 'Ausführlichkeit des Fehlerprotokolls';
|
||||
$lng['serversettings']['errorlog_level']['description'] = 'Steuert die Ausführlichkeit des Fehlerprotokolls. Voreinstellung ist "warn" bei Apache und "error" bei Nginx.';
|
||||
$lng['serversettings']['letsencryptecc']['title'] = "ECC / ECDSA Zertifikate ausstellen";
|
||||
$lng['serversettings']['letsencryptecc']['description'] = "Wenn eine Schlüsselgröße ausgewählt wird, werden ECC / ECDSA Zertifikate erstellt";
|
||||
|
||||
Reference in New Issue
Block a user