From 4f0c1894a3bac28639cac96a9a213045e16bb99c Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Fri, 29 Jan 2016 14:29:30 +0100 Subject: [PATCH 01/30] Initial version of let's encrypt renewal cron Signed-off-by: Florian Aders --- install/froxlor.sql | 6 +- .../updates/froxlor/0.9/update_0.9.inc.php | 15 + lib/classes/ssl/class.lescript.php | 477 ++++++++++++++++++ scripts/jobs/cron_letsencrypt.php | 73 +++ 4 files changed, 570 insertions(+), 1 deletion(-) create mode 100644 lib/classes/ssl/class.lescript.php create mode 100644 scripts/jobs/cron_letsencrypt.php diff --git a/install/froxlor.sql b/install/froxlor.sql index eaf55166..0e9819b2 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -509,6 +509,8 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES ('system', 'dns_createhostnameentry', '0'), ('system', 'send_cron_errors', '0'), ('system', 'apacheitksupport', '0'), + ('system', 'leprivatekey', 'unset'), + ('system', 'lepublickey', 'unset'), ('panel', 'decimal_places', '4'), ('panel', 'adminmail', 'admin@SERVERNAME'), ('panel', 'phpmyadmin_url', ''), @@ -539,7 +541,7 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES ('panel', 'password_numeric', '0'), ('panel', 'password_special_char_required', '0'), ('panel', 'password_special_char', '!?<>§$%+#=@'), - ('panel', 'version', '0.9.34.2'); + ('panel', 'version', '0.9.35-dev1'); DROP TABLE IF EXISTS `panel_tasks`; @@ -822,6 +824,8 @@ CREATE TABLE IF NOT EXISTS `domain_ssl_settings` ( `ssl_key_file` mediumtext NOT NULL, `ssl_ca_file` mediumtext, `ssl_cert_chainfile` mediumtext, + `letsencrypt` int(11) NOT NULL DEFAULT '0', + `expirationdate` datetime DEFAULT NULL PRIMARY KEY (`id`) ) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index 3a4f870c..f57c9e4d 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3020,3 +3020,18 @@ if (isFroxlorVersion('0.9.34.1')) { updateToVersion('0.9.34.2'); } + +if (isFroxlorVersion('0.9.34.2')) { + + showUpdateStep("Updating from 0.9.34.2 to 0.9.35-dev1"); + lastStepStatus(0); + showUpdateStep("Adding Let's encrypt - certificate fields"); + Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `letsencrypt` INT NOT NULL DEFAULT '0' AFTER `ssl_cert_chainfile`"); + Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `expirationdate` DATETIME NULL AFTER `letsencrypt`;"); + Settings::AddNew("system.leprivatekey", 'unset'); + Settings::AddNew("system.lepublickey", 'unset'); + lastStepStatus(0); + + updateToVersion('0.9.35-dev1'); +} + diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php new file mode 100644 index 00000000..d7252cbe --- /dev/null +++ b/lib/classes/ssl/class.lescript.php @@ -0,0 +1,477 @@ + +# 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 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 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 +{ + #public $ca = 'https://acme-v01.api.letsencrypt.org'; + public $ca = 'https://acme-staging.api.letsencrypt.org'; // testing + public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf'; + public $countryCode = 'DE'; + public $state = "Germany"; + + private $certificatesDir; + private $webRootDir; + + private $debugHandler; + private $client; + private $accountKeyPath; + + public function __construct($certificatesDir, $webRootDir, $debugHandler) + { + $this->certificatesDir = $certificatesDir; + $this->webRootDir = $webRootDir; + $this->debugHandler = $debugHandler; + $this->client = new Client($this->ca); + } + + public function initAccount() + { + + $private = Settings::Get('system.leprivatekey'); + if (!$private || $private == 'unset') { + + // generate and save new private key for account + // --------------------------------------------- + + $this->log('Starting new account registration'); + list($private, $public) = $this->generateKey(); + Settings::Set('system.leprivatekey', $private); + Settings::Set('system.lepublickey', $public); + $this->postNewReg(); + $this->log('New account certificate registered'); + + } else { + + $this->log('Account already registered. Continuing.'); + + } + } + + public function signDomains(array $domains, $domainkey = null) + { + $this->log('Starting certificate generation process for domains'); + + $privateAccountKey = openssl_pkey_get_private(Settings::Get('system.leprivatekey')); + $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)) + ); + + // choose http-01 challange 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 = $this->webRootDir.'/.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($payload !== trim(@file_get_contents($uri))) { + throw new \RuntimeException("Please check $uri - token not available"); + } + + $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 + do { + if(empty($result['status']) || $result['status'] == "invalid") { + throw new \RuntimeException("Verification ended with error: ".json_encode($result)); + } + $ended = !($result['status'] === "pending"); + + if(!$ended) { + $this->log("Verification pending, sleeping 1s"); + sleep(1); + } + + $result = $this->client->get($location); + + } while (!$ended); + + $this->log("Verification ended with status: ${result['status']}"); + @unlink($tokenPath); + } + + // requesting certificate + // ---------------------- + + // generate private key for domain if not exist + if(!is_null($domainkey)) { + list($domainkey, $public) = $this->generateKey(); + } + + // load domain key + $privateDomainKey = openssl_pkey_get_private($domainkey); + + $this->client->getLastLinks(); + + // request certificates creation + $result = $this->signedRequest( + "/acme/new-cert", + array('resource' => 'new-cert', 'csr' => $this->generateCSR($privateDomainKey, $domains)) + ); + 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); + + } else if ($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'); + + $this->log("Saving fullchain.pem"); + $fullchain = implode("\n", $certificates); + + $this->log("Saving cert.pem"); + $crt = array_shift($certificates); + + $this->log("Saving chain.pem"); + $chain = implode("\n", $certificates); + + $this->log("Done !!§§!"); + return array('fullchain' => $fullchain, 'crt' => $crt, 'chain' => $chain, 'key' => $privateDomainKey); + } + + private function parsePemFromBody($body) + { + $pem = chunk_split(base64_encode($body), 64, "\n"); + return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; + } + + private function getDomainPath($domain) + { + return $this->certificatesDir.'/'.$domain.'/'; + } + + private function postNewReg() + { + $this->log('Sending registration to letsencrypt server'); + + return $this->signedRequest( + '/acme/new-reg', + array('resource' => 'new-reg', 'agreement' => $this->license) + ); + } + + 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 = 2048 +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" => $this->state, + "C" => $this->countryCode, + "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" => 4096, + )); + + 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(Settings::Get('system.leprivatekey')); + $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) + { + fwrite($this->debugHandler, 'letsencrypt ' . $message); + } +} + +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]; + } +} + +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, '-_', '+/')); + } +} diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php new file mode 100644 index 00000000..c92d04be --- /dev/null +++ b/scripts/jobs/cron_letsencrypt.php @@ -0,0 +1,73 @@ + + * @author Froxlor team (2016-) + * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt + * @package Cron + * + * @since 0.9.35 + * + */ + +fwrite($debugHandler, "updating let's encrypt certificates\n"); + +$certificates_stmt = Database::query(" + SELECT domssl.`id`, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, dom.`documentroot` + FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` as domssl, `" . TABLE_PANEL_DOMAINS . "` as dom WHERE domssl.domainid = dom.id AND domssl.letsencrypt = 1 +"); + +$upd_stmt = Database::prepare(" + UPDATE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` SET `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, expirationdate = :expirationdate WHERE `id` = :id +"); + +while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { + + # Only renew let's encrypt certificate for domains where a documentroot + # already exists + if (file_exists($certrow['documentroot']) + && is_dir($certrow['documentroot']) + ) { + fwrite($debugHandler, "updating " . $certrow['domain'] . "\n"); + # Parse the old certificate + $x509data = openssl_x509_parse($certrow['ssl_cert_file']); + + # We are interessted in the old SAN - data + $san = explode(', ', $x509data['extensions']['subjectAltName']); + $domains = array(); + foreach($san as $dnsname) { + $domains[] = substr($dnsname, 4); + } + + try { + # Initialize Lescript with documentroot + $le = new lescript($certrow['documentroot'], $debugHandler); + # 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($upd_stmt, array( + 'crt' => $return['crt'], + 'key' => $return['key'], + 'ca' => $return['fullchain'], + 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']), + 'id' => $certrow['id']) + ); + } catch (\Exception $e) { + fwrite($debugHandler, 'letsencrypt exception: ' . $e->getMessage()); + } + } else { + fwrite($debugHandler, 'documentroot ' . $certrow['documentroot'] . ' does not exist' . "\n"); + } +} From d45e9e63e6adf5c63663b8ec8c19b26c19633719 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Fri, 29 Jan 2016 16:52:58 +0100 Subject: [PATCH 02/30] Fixed a few obvious bugs Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 18 ++++++------------ scripts/jobs/cron_letsencrypt.php | 4 ++++ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index d7252cbe..0d1de798 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -34,16 +34,14 @@ class lescript public $countryCode = 'DE'; public $state = "Germany"; - private $certificatesDir; private $webRootDir; private $debugHandler; private $client; private $accountKeyPath; - public function __construct($certificatesDir, $webRootDir, $debugHandler) + public function __construct($webRootDir, $debugHandler) { - $this->certificatesDir = $certificatesDir; $this->webRootDir = $webRootDir; $this->debugHandler = $debugHandler; $this->client = new Client($this->ca); @@ -59,9 +57,9 @@ class lescript // --------------------------------------------- $this->log('Starting new account registration'); - list($private, $public) = $this->generateKey(); - Settings::Set('system.leprivatekey', $private); - Settings::Set('system.lepublickey', $public); + $keys = $this->generateKey(); + Settings::Set('system.leprivatekey', $keys['private']); + Settings::Set('system.lepublickey', $keys['public']); $this->postNewReg(); $this->log('New account certificate registered'); @@ -174,7 +172,8 @@ class lescript // generate private key for domain if not exist if(!is_null($domainkey)) { - list($domainkey, $public) = $this->generateKey(); + $keys = $this->generateKey(); + $domainkey = $keys['private']; } // load domain key @@ -245,11 +244,6 @@ class lescript return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; } - private function getDomainPath($domain) - { - return $this->certificatesDir.'/'.$domain.'/'; - } - private function postNewReg() { $this->log('Sending registration to letsencrypt server'); diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index c92d04be..e6863cdf 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -50,6 +50,10 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { try { # Initialize Lescript with documentroot $le = new lescript($certrow['documentroot'], $debugHandler); + + # Initialize Lescript + $le->initAccount(); + # Request the new certificate (old key may be used) $return = $le->signDomains($domains, $certrow['ssl_key_file']); From ed2837f1db8f027a1b2a94cc4c2cc6363ce83ae1 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Fri, 29 Jan 2016 16:59:00 +0100 Subject: [PATCH 03/30] Added linebreak to logmessage of class Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index 0d1de798..30358ed0 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -357,7 +357,7 @@ keyUsage = nonRepudiation, digitalSignature, keyEncipherment'); protected function log($message) { - fwrite($this->debugHandler, 'letsencrypt ' . $message); + fwrite($this->debugHandler, 'letsencrypt ' . $message . "\n"); } } From 2e7dd6f2125df8fb2897c20b325c442ee51d4433 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 10:56:08 +0100 Subject: [PATCH 04/30] Changed comments to // to match the rest of Froxlor and made small improvements to the accountKey Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 79 +++++++++++++++--------------- scripts/jobs/cron_letsencrypt.php | 22 ++++----- 2 files changed, 50 insertions(+), 51 deletions(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index 30358ed0..26a41e62 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -1,34 +1,34 @@ -# 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 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 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. +// Copyright (c) 2015, Stanislav Humplik +// 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 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 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 +// This file is copied from https://github.com/analogic/lescript +// and modified to work without files and integrate in Froxlor class lescript { - #public $ca = 'https://acme-v01.api.letsencrypt.org'; + //public $ca = 'https://acme-v01.api.letsencrypt.org'; public $ca = 'https://acme-staging.api.letsencrypt.org'; // testing public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf'; public $countryCode = 'DE'; @@ -38,7 +38,7 @@ class lescript private $debugHandler; private $client; - private $accountKeyPath; + private $accountKey; public function __construct($webRootDir, $debugHandler) { @@ -49,9 +49,9 @@ class lescript public function initAccount() { - - $private = Settings::Get('system.leprivatekey'); - if (!$private || $private == 'unset') { + // Let's see if we have the private accountkey + $this->accountKey = Settings::Get('system.leprivatekey'); + if (!$this->accountKey || $this->accountKey == 'unset') { // generate and save new private key for account // --------------------------------------------- @@ -60,6 +60,7 @@ class lescript $keys = $this->generateKey(); Settings::Set('system.leprivatekey', $keys['private']); Settings::Set('system.lepublickey', $keys['public']); + $this->accountKey = $keys['private']; $this->postNewReg(); $this->log('New account certificate registered'); @@ -74,7 +75,7 @@ class lescript { $this->log('Starting certificate generation process for domains'); - $privateAccountKey = openssl_pkey_get_private(Settings::Get('system.leprivatekey')); + $privateAccountKey = openssl_pkey_get_private($this->accountKey); $accountKeyDetails = openssl_pkey_get_details($privateAccountKey); // start domains authentication @@ -148,6 +149,8 @@ class lescript ); // waiting loop + // we wait for a maximum of 30 seconds to avoid endless loops + $count = 0; do { if(empty($result['status']) || $result['status'] == "invalid") { throw new \RuntimeException("Verification ended with error: ".json_encode($result)); @@ -157,11 +160,12 @@ class lescript if(!$ended) { $this->log("Verification pending, sleeping 1s"); sleep(1); + $count++; } $result = $this->client->get($location); - } while (!$ended); + } while (!$ended && $count < 30); $this->log("Verification ended with status: ${result['status']}"); @unlink($tokenPath); @@ -225,16 +229,11 @@ class lescript if(empty($certificates)) throw new \RuntimeException('No certificates generated'); - $this->log("Saving fullchain.pem"); $fullchain = implode("\n", $certificates); - - $this->log("Saving cert.pem"); $crt = array_shift($certificates); - - $this->log("Saving chain.pem"); $chain = implode("\n", $certificates); - $this->log("Done !!§§!"); + $this->log("Done, returning new certificates and key"); return array('fullchain' => $fullchain, 'crt' => $crt, 'chain' => $chain, 'key' => $privateDomainKey); } @@ -320,7 +319,7 @@ keyUsage = nonRepudiation, digitalSignature, keyEncipherment'); private function signedRequest($uri, array $payload) { - $privateKey = openssl_pkey_get_private(Settings::Get('system.leprivatekey')); + $privateKey = openssl_pkey_get_private($this->accountKey); $details = openssl_pkey_get_details($privateKey); $header = array( diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index e6863cdf..bbd6f7c7 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -31,16 +31,16 @@ $upd_stmt = Database::prepare(" while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { - # Only renew let's encrypt certificate for domains where a documentroot - # already exists + // Only renew let's encrypt certificate for domains where a documentroot + // already exists if (file_exists($certrow['documentroot']) && is_dir($certrow['documentroot']) ) { fwrite($debugHandler, "updating " . $certrow['domain'] . "\n"); - # Parse the old certificate + // Parse the old certificate $x509data = openssl_x509_parse($certrow['ssl_cert_file']); - # We are interessted in the old SAN - data + // We are interessted in the old SAN - data $san = explode(', ', $x509data['extensions']['subjectAltName']); $domains = array(); foreach($san as $dnsname) { @@ -48,19 +48,19 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { } try { - # Initialize Lescript with documentroot + // Initialize Lescript with documentroot $le = new lescript($certrow['documentroot'], $debugHandler); - # Initialize Lescript + // Initialize Lescript $le->initAccount(); - # Request the new certificate (old key may be used) + // Request the new certificate (old key may be used) $return = $le->signDomains($domains, $certrow['ssl_key_file']); - # We are interessted in the expirationdate + // We are interessted in the expirationdate $newcert = openssl_x509_parse($return['crt']); - # Store the new data + // Store the new data Database::pexecute($upd_stmt, array( 'crt' => $return['crt'], 'key' => $return['key'], @@ -69,9 +69,9 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { 'id' => $certrow['id']) ); } catch (\Exception $e) { - fwrite($debugHandler, 'letsencrypt exception: ' . $e->getMessage()); + fwrite($debugHandler, 'letsencrypt exception: ' . $e->getMessage() . "\n"); } } else { - fwrite($debugHandler, 'documentroot ' . $certrow['documentroot'] . ' does not exist' . "\n"); + fwrite($debugHandler, 'letsencrypt skipped because documentroot ' . $certrow['documentroot'] . ' does not exist' . "\n"); } } From 67df9dbf6ba0df71c6c03dd2fd9e9ab9ff6b1e9e Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 13:06:42 +0100 Subject: [PATCH 05/30] Experiment with accountkeys per customer Signed-off-by: Florian Aders --- install/froxlor.sql | 4 +++- install/updates/froxlor/0.9/update_0.9.inc.php | 2 ++ lib/classes/ssl/class.lescript.php | 10 ++++++---- scripts/jobs/cron_letsencrypt.php | 8 +++++--- 4 files changed, 16 insertions(+), 8 deletions(-) diff --git a/install/froxlor.sql b/install/froxlor.sql index 0e9819b2..09f7f0ac 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -194,6 +194,8 @@ CREATE TABLE `panel_customers` ( `theme` varchar(255) NOT NULL default 'Sparkle', `custom_notes` text, `custom_notes_show` tinyint(1) NOT NULL default '0', + `lepublickey` text DEFAULT NULL, + `leprivatekey` text DEFAULT NULL, PRIMARY KEY (`customerid`), UNIQUE KEY `loginname` (`loginname`) ) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; @@ -825,7 +827,7 @@ CREATE TABLE IF NOT EXISTS `domain_ssl_settings` ( `ssl_ca_file` mediumtext, `ssl_cert_chainfile` mediumtext, `letsencrypt` int(11) NOT NULL DEFAULT '0', - `expirationdate` datetime DEFAULT NULL + `expirationdate` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index f57c9e4d..e7b94e4c 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3028,6 +3028,8 @@ if (isFroxlorVersion('0.9.34.2')) { showUpdateStep("Adding Let's encrypt - certificate fields"); Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `letsencrypt` INT NOT NULL DEFAULT '0' AFTER `ssl_cert_chainfile`"); Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `expirationdate` DATETIME NULL AFTER `letsencrypt`;"); + Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `lepublickey` TEXT DEFAULT NULL AFTER `custom_notes_show`"); + Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `leprivatekey` TEXT DEFAULT NULL AFTER `lepublickey`;"); Settings::AddNew("system.leprivatekey", 'unset'); Settings::AddNew("system.lepublickey", 'unset'); lastStepStatus(0); diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index 26a41e62..77d346d3 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -47,10 +47,10 @@ class lescript $this->client = new Client($this->ca); } - public function initAccount() + public function initAccount($certrow) { // Let's see if we have the private accountkey - $this->accountKey = Settings::Get('system.leprivatekey'); + $this->accountKey = $certrow['leprivatekey']; if (!$this->accountKey || $this->accountKey == 'unset') { // generate and save new private key for account @@ -58,8 +58,10 @@ class lescript $this->log('Starting new account registration'); $keys = $this->generateKey(); - Settings::Set('system.leprivatekey', $keys['private']); - Settings::Set('system.lepublickey', $keys['public']); + $upd_stmt = Database::prepare(" + UPDATE `".TABLE_PANEL_CUSTOMERS."` SET `lepublickey` = :public AND `leprivatekey` = :private WHERE `customerid` = :customerid; + "); + Database::pexecute($upd_stmt, array('public' => $keys['public'], 'private' => $keys['private'], 'customerid' => $certrow['customerid'])); $this->accountKey = $keys['private']; $this->postNewReg(); $this->log('New account certificate registered'); diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index bbd6f7c7..4f93bfdc 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -21,8 +21,10 @@ fwrite($debugHandler, "updating let's encrypt certificates\n"); $certificates_stmt = Database::query(" - SELECT domssl.`id`, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, dom.`documentroot` - FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` as domssl, `" . TABLE_PANEL_DOMAINS . "` as dom WHERE domssl.domainid = dom.id AND domssl.letsencrypt = 1 + SELECT domssl.`id`, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, dom.`documentroot`, + cust.`leprivatekey`, cust.`lepublickey`, cust.customerid + FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` as domssl, `" . TABLE_PANEL_DOMAINS . "` as dom, `" . TABLE_PANEL_CUSTOMERS . "` as cust + WHERE domssl.domainid = dom.id AND dom.customerid = cust.customerid AND domssl.letsencrypt = 1 "); $upd_stmt = Database::prepare(" @@ -52,7 +54,7 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { $le = new lescript($certrow['documentroot'], $debugHandler); // Initialize Lescript - $le->initAccount(); + $le->initAccount($certrow); // Request the new certificate (old key may be used) $return = $le->signDomains($domains, $certrow['ssl_key_file']); From e0e1085c736a5e7d2fa50fe36d4f7db76a4b7f7e Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 13:14:51 +0100 Subject: [PATCH 06/30] Fix your SQL, dude Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index 77d346d3..ca2d320d 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -59,7 +59,7 @@ class lescript $this->log('Starting new account registration'); $keys = $this->generateKey(); $upd_stmt = Database::prepare(" - UPDATE `".TABLE_PANEL_CUSTOMERS."` SET `lepublickey` = :public AND `leprivatekey` = :private WHERE `customerid` = :customerid; + UPDATE `".TABLE_PANEL_CUSTOMERS."` SET `lepublickey` = :public, `leprivatekey` = :private WHERE `customerid` = :customerid; "); Database::pexecute($upd_stmt, array('public' => $keys['public'], 'private' => $keys['private'], 'customerid' => $certrow['customerid'])); $this->accountKey = $keys['private']; From 44d08d6aa916718fb6b943aa5d23dd8c985266d2 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 13:59:34 +0100 Subject: [PATCH 07/30] Add let's encrypt to customer domain - GUI Signed-off-by: Florian Aders --- customer_domains.php | 48 +++++++++++++++---- .../updates/froxlor/0.9/update_0.9.inc.php | 1 + lib/classes/ssl/class.lescript.php | 5 ++ .../domains/formfield.domains_add.php | 10 ++++ .../domains/formfield.domains_edit.php | 10 ++++ lng/english.lng.php | 6 +++ scripts/jobs/cron_letsencrypt.php | 33 ++++++++----- .../customer/domains/domains_domain.tpl | 3 ++ 8 files changed, 95 insertions(+), 21 deletions(-) diff --git a/customer_domains.php b/customer_domains.php index 01311329..b32f7b48 100644 --- a/customer_domains.php +++ b/customer_domains.php @@ -36,7 +36,7 @@ if ($page == 'overview') { 'd.domain' => $lng['domains']['domainname'] ); $paging = new paging($userinfo, TABLE_PANEL_DOMAINS, $fields); - $domains_stmt = Database::prepare("SELECT `d`.`id`, `d`.`customerid`, `d`.`domain`, `d`.`documentroot`, `d`.`isemaildomain`, `d`.`caneditdomain`, `d`.`iswildcarddomain`, `d`.`parentdomainid`, `ad`.`id` AS `aliasdomainid`, `ad`.`domain` AS `aliasdomain`, `da`.`id` AS `domainaliasid`, `da`.`domain` AS `domainalias` FROM `" . TABLE_PANEL_DOMAINS . "` `d` + $domains_stmt = Database::prepare("SELECT `d`.`id`, `d`.`customerid`, `d`.`domain`, `d`.`documentroot`, `d`.`isemaildomain`, `d`.`caneditdomain`, `d`.`iswildcarddomain`, `d`.`parentdomainid`, `d`.`letsencrypt`, `ad`.`id` AS `aliasdomainid`, `ad`.`domain` AS `aliasdomain`, `da`.`id` AS `domainaliasid`, `da`.`domain` AS `domainalias` FROM `" . TABLE_PANEL_DOMAINS . "` `d` LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `ad` ON `d`.`aliasdomain`=`ad`.`id` LEFT JOIN `" . TABLE_PANEL_DOMAINS . "` `da` ON `da`.`aliasdomain`=`d`.`id` WHERE `d`.`customerid`= :customerid @@ -71,7 +71,7 @@ if ($page == 'overview') { $ssl_stmt = Database::prepare("SELECT * FROM `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` WHERE `domainid` = :domainid"); Database::pexecute($ssl_stmt, array("domainid" => $row['id'])); $ssl_result = $ssl_stmt->fetch(PDO::FETCH_ASSOC); - if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '') { + if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '' && $row['letsencrypt'] == 0) { // own certificate (ssl_customer_green) $row['domain_hascert'] = 1; } else { @@ -303,7 +303,7 @@ if ($page == 'overview') { $ssl_redirect = '0'; if (isset($_POST['ssl_redirect']) && $_POST['ssl_redirect'] == '1') { - // a ssl-redirect only works of there actually is a + // a ssl-redirect only works if there actually is a // ssl ip/port assigned to the domain if (domainHasSslIpPort($domain_check['id']) == true) { $ssl_redirect = '1'; @@ -313,6 +313,17 @@ if ($page == 'overview') { } } + $letsencrypt = '0'; + if (isset($_POST['letsencrypt']) && $_POST['letsencrypt'] == '1') { + // let's encrypt only works if there actually is a + // ssl ip/port assigned to the domain + if (domainHasSslIpPort($domain_check['id']) == true) { + $letsencrypt = '1'; + } else { + standard_error('letsencryptonlypossiblewithsslipport'); + } + } + if ($path == '') { standard_error('patherror'); } elseif ($subdomain == '') { @@ -354,7 +365,8 @@ if ($page == 'overview') { `speciallogfile` = :speciallogfile, `specialsettings` = :specialsettings, `ssl_redirect` = :ssl_redirect, - `phpsettingid` = :phpsettingid" + `phpsettingid` = :phpsettingid, + `letsencrypt` = :letsencrypt" ); $params = array( "customerid" => $userinfo['customerid'], @@ -370,7 +382,8 @@ if ($page == 'overview') { "speciallogfile" => $domain_check['speciallogfile'], "specialsettings" => $domain_check['specialsettings'], "ssl_redirect" => $ssl_redirect, - "phpsettingid" => $phpsid_result['phpsettingid'] + "phpsettingid" => $phpsid_result['phpsettingid'], + "letsencrypt" => $letsencrypt ); Database::pexecute($stmt, $params); @@ -403,7 +416,7 @@ if ($page == 'overview') { redirectTo($filename, array('page' => $page, 's' => $s)); } } else { - $stmt = Database::prepare("SELECT `id`, `domain`, `documentroot`, `ssl_redirect`,`isemaildomain` FROM `" . TABLE_PANEL_DOMAINS . "` + $stmt = Database::prepare("SELECT `id`, `domain`, `documentroot`, `ssl_redirect`,`isemaildomain`,`letsencrypt` FROM `" . TABLE_PANEL_DOMAINS . "` WHERE `customerid` = :customerid AND `parentdomainid` = '0' AND `email_only` = '0' @@ -465,7 +478,7 @@ if ($page == 'overview') { } elseif ($action == 'edit' && $id != 0) { $stmt = Database::prepare("SELECT `d`.`id`, `d`.`customerid`, `d`.`domain`, `d`.`documentroot`, `d`.`isemaildomain`, `d`.`wwwserveralias`, `d`.`iswildcarddomain`, - `d`.`parentdomainid`, `d`.`ssl_redirect`, `d`.`aliasdomain`, `d`.`openbasedir`, `d`.`openbasedir_path`, `pd`.`subcanemaildomain` + `d`.`parentdomainid`, `d`.`ssl_redirect`, `d`.`aliasdomain`, `d`.`openbasedir`, `d`.`openbasedir_path`, `d`.`letsencrypt`, `pd`.`subcanemaildomain` FROM `" . TABLE_PANEL_DOMAINS . "` `d`, `" . TABLE_PANEL_DOMAINS . "` `pd` WHERE `d`.`customerid` = :customerid AND `d`.`id` = :id @@ -545,7 +558,7 @@ if ($page == 'overview') { } if (isset($_POST['ssl_redirect']) && $_POST['ssl_redirect'] == '1') { - // a ssl-redirect only works of there actually is a + // a ssl-redirect only works if there actually is a // ssl ip/port assigned to the domain if (domainHasSslIpPort($id) == true) { $ssl_redirect = '1'; @@ -557,6 +570,18 @@ if ($page == 'overview') { $ssl_redirect = '0'; } + if (isset($_POST['letsencrypt']) && $_POST['letsencrypt'] == '1') { + // let's encrypt only works if there actually is a + // ssl ip/port assigned to the domain + if (domainHasSslIpPort($id) == true) { + $letsencrypt = '1'; + } else { + standard_error('letsencryptonlypossiblewithsslipport'); + } + } else { + $letsencrypt = '0'; + } + if ($path == '') { standard_error('patherror'); } else { @@ -580,7 +605,8 @@ if ($page == 'overview') { || $iswildcarddomain != $result['iswildcarddomain'] || $aliasdomain != $result['aliasdomain'] || $openbasedir_path != $result['openbasedir_path'] - || $ssl_redirect != $result['ssl_redirect']) { + || $ssl_redirect != $result['ssl_redirect'] + || $letsencrypt != $result['letsencrypt']) { $log->logAction(USR_ACTION, LOG_INFO, "edited domain '" . $idna_convert->decode($result['domain']) . "'"); $stmt = Database::prepare("UPDATE `" . TABLE_PANEL_DOMAINS . "` SET @@ -590,7 +616,8 @@ if ($page == 'overview') { `iswildcarddomain`= :iswildcarddomain, `aliasdomain`= :aliasdomain, `openbasedir_path`= :openbasedir_path, - `ssl_redirect`= :ssl_redirect + `ssl_redirect`= :ssl_redirect, + `letsencrypt`= :letsencrypt WHERE `customerid`= :customerid AND `id`= :id" ); @@ -602,6 +629,7 @@ if ($page == 'overview') { "aliasdomain" => ($aliasdomain != 0 && $alias_check == 0) ? $aliasdomain : null, "openbasedir_path" => $openbasedir_path, "ssl_redirect" => $ssl_redirect, + "letsencrypt" => $letsencrypt, "customerid" => $userinfo['customerid'], "id" => $id ); diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index e7b94e4c..e80708ba 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3030,6 +3030,7 @@ if (isFroxlorVersion('0.9.34.2')) { Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `expirationdate` DATETIME NULL AFTER `letsencrypt`;"); Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `lepublickey` TEXT DEFAULT NULL AFTER `custom_notes_show`"); Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `leprivatekey` TEXT DEFAULT NULL AFTER `lepublickey`;"); + Database::query("ALTER TABLE `".TABLE_PANEL_DOMAINS."` ADD `letsencrypt` INT NOT NULL DEFAULT '0' AFTER `ismainbutsubto`;"); Settings::AddNew("system.leprivatekey", 'unset'); Settings::AddNew("system.lepublickey", 'unset'); lastStepStatus(0); diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index ca2d320d..b043992b 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -75,6 +75,11 @@ class lescript public function signDomains(array $domains, $domainkey = null) { + + if (!$this->accountKey) { + throw new \RuntimeException("Account not initiated"); + } + $this->log('Starting certificate generation process for domains'); $privateAccountKey = openssl_pkey_get_private($this->accountKey); diff --git a/lib/formfields/customer/domains/formfield.domains_add.php b/lib/formfields/customer/domains/formfield.domains_add.php index 55a1a67c..5d8fd271 100644 --- a/lib/formfields/customer/domains/formfield.domains_add.php +++ b/lib/formfields/customer/domains/formfield.domains_add.php @@ -70,6 +70,16 @@ return array( ), 'value' => array() ), + 'useletsencrypt' => array( + 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), + 'label' => $lng['domains']['letsencrypt']['title'], + 'desc' => $lng['domains']['letsencrypt']['description'], + 'type' => 'checkbox', + 'values' => array( + array ('label' => $lng['panel']['yes'], 'value' => '1') + ), + 'value' => array() + ), 'openbasedir_path' => array( 'label' => $lng['domain']['openbasedirpath'], 'type' => 'select', diff --git a/lib/formfields/customer/domains/formfield.domains_edit.php b/lib/formfields/customer/domains/formfield.domains_edit.php index 7d809532..005d0b4c 100644 --- a/lib/formfields/customer/domains/formfield.domains_edit.php +++ b/lib/formfields/customer/domains/formfield.domains_edit.php @@ -86,6 +86,16 @@ return array( ), 'value' => array($result['ssl_redirect']) ), + 'useletsencrypt' => array( + 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? (domainHasSslIpPort($result['id']) ? true : false) : false) : false), + 'label' => $lng['domains']['letsencrypt']['title'], + 'desc' => $lng['domains']['letsencrypt']['description'], + 'type' => 'checkbox', + 'values' => array( + array ('label' => $lng['panel']['yes'], 'value' => '1') + ), + 'value' => array($result['letsencrypt']) + ), 'openbasedir_path' => array( 'visible' => ($result['openbasedir'] == '1') ? true : false, 'label' => $lng['domain']['openbasedirpath'], diff --git a/lng/english.lng.php b/lng/english.lng.php index d369cb3b..4d8c78b3 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1924,3 +1924,9 @@ $lng['opcacheinfo']['blacklist'] = 'Blacklist'; $lng['opcacheinfo']['novalue'] = 'no value'; $lng['opcacheinfo']['true'] = 'true'; $lng['opcacheinfo']['false'] = 'false'; + +// Added for let's encrypt +$lng['domains']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; +$lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. This certificate will be created and renewed automatically'; +$lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is only possible when the domain has at least one ssl-enabled IP/port combination assigned.'; +$lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; \ No newline at end of file diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index 4f93bfdc..64772ffa 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -21,10 +21,10 @@ fwrite($debugHandler, "updating let's encrypt certificates\n"); $certificates_stmt = Database::query(" - SELECT domssl.`id`, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, dom.`documentroot`, - cust.`leprivatekey`, cust.`lepublickey`, cust.customerid - FROM `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` as domssl, `" . TABLE_PANEL_DOMAINS . "` as dom, `" . TABLE_PANEL_CUSTOMERS . "` as cust - WHERE domssl.domainid = dom.id AND dom.customerid = cust.customerid AND domssl.letsencrypt = 1 + SELECT domssl.`id`, domssl.expirationdate, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, + dom.`documentroot`, cust.`leprivatekey`, cust.`lepublickey`, cust.customerid + FROM `".TABLE_PANEL_CUSTOMERS."` as cust, `".TABLE_PANEL_DOMAINS."` dom LEFT JOIN `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` domssl ON (dom.id = domssl.domainid) + WHERE dom.customerid = cust.customerid AND dom.letsencrypt = 1 AND (domssl.expirationdate < DATE_ADD(NOW(), INTERVAL 30 DAY) OR domssl.expirationdate IS NULL) "); $upd_stmt = Database::prepare(" @@ -39,14 +39,25 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { && is_dir($certrow['documentroot']) ) { fwrite($debugHandler, "updating " . $certrow['domain'] . "\n"); - // Parse the old certificate - $x509data = openssl_x509_parse($certrow['ssl_cert_file']); - // We are interessted in the old SAN - data - $san = explode(', ', $x509data['extensions']['subjectAltName']); - $domains = array(); - foreach($san as $dnsname) { - $domains[] = substr($dnsname, 4); + if ($certrow['ssl_cert_file']) { + fwrite($debugHandler, "letsencrypt using old key / SAN for " . $certrow['domain'] . "\n"); + // Parse the old certificate + $x509data = openssl_x509_parse($certrow['ssl_cert_file']); + + // We are interessted in the old SAN - data + $san = explode(', ', $x509data['extensions']['subjectAltName']); + $domains = array(); + foreach($san as $dnsname) { + $domains[] = substr($dnsname, 4); + } + } else { + fwrite($debugHandler, "letsencrypt generating new key / SAN for " . $certrow['domain'] . "\n"); + $domains = array($certrow['domain']); + // Add www. for SAN + if ($certrow['wwwserveralias'] == 1) { + $domains[] = 'www.' . $certrow['domain']; + } } try { diff --git a/templates/Sparkle/customer/domains/domains_domain.tpl b/templates/Sparkle/customer/domains/domains_domain.tpl index 681e1487..4e63a9d9 100644 --- a/templates/Sparkle/customer/domains/domains_domain.tpl +++ b/templates/Sparkle/customer/domains/domains_domain.tpl @@ -20,6 +20,9 @@ {$lng['panel']['ssleditor']}   + + {$lng['panel']['letsencrypt']} + ({$lng['domains']['isassigneddomain']})  From 7b5a4259133c19bb30802e4790e3d0022c5aaa8f Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 14:05:12 +0100 Subject: [PATCH 08/30] Always hide the SSL - editor if using let's encrypt Signed-off-by: Florian Aders --- customer_domains.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/customer_domains.php b/customer_domains.php index b32f7b48..1c89ddfd 100644 --- a/customer_domains.php +++ b/customer_domains.php @@ -74,7 +74,7 @@ if ($page == 'overview') { if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '' && $row['letsencrypt'] == 0) { // own certificate (ssl_customer_green) $row['domain_hascert'] = 1; - } else { + } elseif ($row['letsencrypt'] == 0) { // check if it's parent has one set (shared) if ($row['parentdomainid'] != 0) { $ssl_stmt = Database::prepare("SELECT * FROM `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` WHERE `domainid` = :domainid"); From cb97ff0dc7aee50ffdfebf15610ff27fd9dbcee7 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 14:11:58 +0100 Subject: [PATCH 09/30] Add icon and fixed list Signed-off-by: Florian Aders --- customer_domains.php | 6 +++--- .../Sparkle/assets/img/icons/ssl_letsencrypt.png | Bin 0 -> 4003 bytes 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 templates/Sparkle/assets/img/icons/ssl_letsencrypt.png diff --git a/customer_domains.php b/customer_domains.php index 1c89ddfd..c8ec610b 100644 --- a/customer_domains.php +++ b/customer_domains.php @@ -71,10 +71,10 @@ if ($page == 'overview') { $ssl_stmt = Database::prepare("SELECT * FROM `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` WHERE `domainid` = :domainid"); Database::pexecute($ssl_stmt, array("domainid" => $row['id'])); $ssl_result = $ssl_stmt->fetch(PDO::FETCH_ASSOC); - if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '' && $row['letsencrypt'] == 0) { + if (is_array($ssl_result) && isset($ssl_result['ssl_cert_file']) && $ssl_result['ssl_cert_file'] != '') { // own certificate (ssl_customer_green) $row['domain_hascert'] = 1; - } elseif ($row['letsencrypt'] == 0) { + } else { // check if it's parent has one set (shared) if ($row['parentdomainid'] != 0) { $ssl_stmt = Database::prepare("SELECT * FROM `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` WHERE `domainid` = :domainid"); @@ -146,7 +146,7 @@ if ($page == 'overview') { // get ssl-ips if activated $show_ssledit = false; - if (Settings::Get('system.use_ssl') == '1' && domainHasSslIpPort($row['id']) && $row['caneditdomain'] == '1') { + if (Settings::Get('system.use_ssl') == '1' && domainHasSslIpPort($row['id']) && $row['caneditdomain'] == '1' && $row['letsencrypt'] == 0) { $show_ssledit = true; } $row = htmlentities_array($row); diff --git a/templates/Sparkle/assets/img/icons/ssl_letsencrypt.png b/templates/Sparkle/assets/img/icons/ssl_letsencrypt.png new file mode 100644 index 0000000000000000000000000000000000000000..ef0d8f64971bd0d31d76fdcd5e792fa292922df7 GIT binary patch literal 4003 zcmeHJX*iqN9)IcBMYRlCY9biL$ZIQN1l8Duilw5qh#<8jjZ_kAbXwD~i(;&u(c0SD z)o$F@R!i-zE;H6^t6~(5QR+r-pSfS|7a#72`#jG3ob$fte}3op{?G56|9SpDU9>fa z2%Z)M003fXfx>X!q6bx!m($we zO2%c@@>${7$wzTmZkA}~oR_du%@5d5yI!yJ?u|C)t}>(@G+Z)n3@MRI_BLq6ZDfHQ zC6dNXnd|LyHG>#+i`9mt{JcBtttms^OB8)iepH-Ca`#Y1RLt*&kOI&5E3URxa3dmO zW^}!U+HcVHMv#KQApY5suM9TNfEhJWhK&!lA{a>)UBQ1An~Z4BDs_Js$FJ?56>0zc z*N(Tv3bXL+)rF*iY4rPnVf3mSmeOj=(^x6~1f7(TyJA+ppiQw}!OuUCEP^-nEyHN7 zYo+YahvnN$8y?sA9hY92^CuEoY-^Yj>skr3fv41PKYiU1dg*aUNV;B4Wz#@D4gP!M zv)?GDw>+{@n=mJ8FUzb4vt`Wea)+tEyC3R|}VX*KZSAAn(NIX_6wlmX= zeIt6r*1R?Oq+jS#|JO4tdry9RiZsoii=UnDkn>kwBK&QaJJU(DHf#O{k3{_$xX$e0 z>yaMpuYLn*-`DYWMoJGko&-E(fVX3DWHd79wjU}0ouU^`O7K!NG=OVKb z8gXKbw{bmkUBld>v1MX0ywW_CExKcNn*!$?ZLZ2@VJrQ6s?&dY%qJw*p*~P@^4yNU zMu(I`f0Y8-5G$x$bep1rt)Ahpk)hnnr3@5$jc||iyx=)q7+AeDulweSM1AdCnd5D{ zt>;%ts*17$UJ=I6yoo*@eh<0A028IHO8@dzt9^;-E2ECNj;}Uv4dLyOYzpSGV@2sY zq_KO9ZZF-jI~IRu44Smw&XYXb!K)=IHVBpF;}WkG_JvomHJ?I!;X*n$1xl(Qdfy-S zhAv9SUo(6a$qIj(*x%NP{!QNe*dL^nyD`D3AA46s9z{m5-ztQ@dy?u@h)9wTz9QEVy^bo;PeahqiMbHS6X}6vz`|UcC6)wH=eiYi0xI)CCasu z7?WZ>gUDz3QPubR@;!Ps>t!dwrwirEdl=IZv`7Bd;IhTU!~V;I)TSukbp*dL{uU4aNw%WfW6QtH}w__$Wuns+zzZsWl4@dC!} zyWPXoU7r4J%C6A06CiW?e%v@GSiSyhloH8Q{6Ul=t@wi^mAe%CqYBT^TiEWQTb?-kO3iBG5G z&)Wfjz!f6rMsPvf=;KL2aGVdxn*gT=kvUEPfPoR6jKc>Is8DZ$A2ApSTWfBEL5V&{ z*rju5bu{?`!JlXmMj<$a**fCG0`LeQn318N0bQR%5JaHjp!A@?;1GQ}684!_pYwfS z24T?85NZGt=7PQmy+EQ6pxSV4xVoAtofxVKGZcgxP<(v#F(|Vy6r3Is=1--P^+AwE zqrqufa1zB2)IcB*pt>fgsj0?6sD&_ssW`e?aESZ?#lJaFgb+N1qa2YG3_ak)d6TYD zkuVr%9QuXa2md!W`5GngGqVpKBm@$I2*K14Py?<3e#ZOY_033>ARK2tF$m{J0Lj6A zFz~=z@Qb^g)q{U8|GqxFcDR3z8(djyp>re_D3Vd|B{=eiB{7(W! z2G){U1>@$rysTHrcwK)pdKX`s=El%TD1jrWH5C9MoJE&YhcCHl1$VatAoAOO@ z$wCBVsq$s!0KipZi8}9S(cS6vOciA>zMmHm+bEG>Z-VJHv9c1}0A2W%H#JpX_~K$P z)oG`ft4^1n#Ylw4C8n}ic|1B+-U-PD6A?1;3zlICj0(M+(Z+@RN#r+e;o#K8pPg(Z z9-*}!m@X+BP~&D}LX?>=v$z5NKin)c7a*`eRn<+ZCTBFjJ=PlMY(1hYNpRL!x^VW6 zv7%y4Y0ZL&h%@GW-Obq|Zbik(mlc!3Svv*0ISoy2+qXSc!@B`d|HuzNmSKIYop063 z?QLECb6i1NOC;h0A%)?hIAYS2x>}FW`$3xlNbM%L0xYX-VJ4F@BG_WIt_U3YDg$oAf2FTLH} z0gt5_+3Sxp`FvsLRuGdhZ4%uA3Tf+f1rwhoqv*J2Cr*hK&7Ij#ZUdOKt{|n(=tOm0 pCfBi7#`OUig(=+QW6=vy$LwU99o;%yJ2=}2Sen|R%1ykk{|kx&Y Date: Sat, 30 Jan 2016 14:13:45 +0100 Subject: [PATCH 10/30] Also: use the new icon Signed-off-by: Florian Aders --- templates/Sparkle/customer/domains/domains_domain.tpl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/Sparkle/customer/domains/domains_domain.tpl b/templates/Sparkle/customer/domains/domains_domain.tpl index 4e63a9d9..a85e74e0 100644 --- a/templates/Sparkle/customer/domains/domains_domain.tpl +++ b/templates/Sparkle/customer/domains/domains_domain.tpl @@ -21,7 +21,7 @@   - {$lng['panel']['letsencrypt']} + {$lng['panel']['letsencrypt']} ({$lng['domains']['isassigneddomain']})  From f3a7a9c342d9245f77b1c9de05ede90db7b8559d Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 14:22:32 +0100 Subject: [PATCH 11/30] Fix formfield Signed-off-by: Florian Aders --- lib/formfields/customer/domains/formfield.domains_add.php | 2 +- lib/formfields/customer/domains/formfield.domains_edit.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/formfields/customer/domains/formfield.domains_add.php b/lib/formfields/customer/domains/formfield.domains_add.php index 5d8fd271..856d1bdd 100644 --- a/lib/formfields/customer/domains/formfield.domains_add.php +++ b/lib/formfields/customer/domains/formfield.domains_add.php @@ -70,7 +70,7 @@ return array( ), 'value' => array() ), - 'useletsencrypt' => array( + 'letsencrypt' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), 'label' => $lng['domains']['letsencrypt']['title'], 'desc' => $lng['domains']['letsencrypt']['description'], diff --git a/lib/formfields/customer/domains/formfield.domains_edit.php b/lib/formfields/customer/domains/formfield.domains_edit.php index 005d0b4c..fc501991 100644 --- a/lib/formfields/customer/domains/formfield.domains_edit.php +++ b/lib/formfields/customer/domains/formfield.domains_edit.php @@ -86,7 +86,7 @@ return array( ), 'value' => array($result['ssl_redirect']) ), - 'useletsencrypt' => array( + 'letsencrypt' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? (domainHasSslIpPort($result['id']) ? true : false) : false) : false), 'label' => $lng['domains']['letsencrypt']['title'], 'desc' => $lng['domains']['letsencrypt']['description'], From c189ad759bfea245df1d94333d5ec7c8d7f04424 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 14:34:26 +0100 Subject: [PATCH 12/30] Add Let's Encrypt to admin - GUI Signed-off-by: Florian Aders --- admin_domains.php | 34 +++++++++++++++---- .../admin/domains/formfield.domains_add.php | 10 ++++++ .../admin/domains/formfield.domains_edit.php | 10 ++++++ 3 files changed, 48 insertions(+), 6 deletions(-) diff --git a/admin_domains.php b/admin_domains.php index e3c2a17f..c897ba01 100644 --- a/admin_domains.php +++ b/admin_domains.php @@ -516,6 +516,11 @@ if ($page == 'domains' $ssl_redirect = (int)$_POST['ssl_redirect']; } + $letsencrypt = 0; + if (isset($_POST['letsencrypt'])) { + $letsencrypt = (int)$_POST['letsencrypt']; + } + $ssl_ipandports = array(); if (isset($_POST['ssl_ipandport']) && !is_array($_POST['ssl_ipandport'])) { $_POST['ssl_ipandport'] = unserialize($_POST['ssl_ipandport']); @@ -547,12 +552,14 @@ if ($page == 'domains' } } else { $ssl_redirect = 0; + $letsencrypt = 0; // we need this for the serialize // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; } } else { $ssl_redirect = 0; + $letsencrypt = 0; // we need this for the serialize // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; @@ -702,7 +709,8 @@ if ($page == 'domains' 'mod_fcgid_maxrequests' => $mod_fcgid_maxrequests, 'specialsettings' => $specialsettings, 'registration_date' => $registration_date, - 'issubof' => $issubof + 'issubof' => $issubof, + 'letsencrypt' => $letsencrypt ); $security_questions = array( @@ -751,7 +759,8 @@ if ($page == 'domains' 'phpsettingid' => $phpsettingid, 'mod_fcgid_starter' => $mod_fcgid_starter, 'mod_fcgid_maxrequests' => $mod_fcgid_maxrequests, - 'ismainbutsubto' => $issubof + 'ismainbutsubto' => $issubof, + 'letsencrypt' => $letsencrypt ); $ins_stmt = Database::prepare(" @@ -782,7 +791,8 @@ if ($page == 'domains' `phpsettingid` = :phpsettingid, `mod_fcgid_starter` = :mod_fcgid_starter, `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests, - `ismainbutsubto` = :ismainbutsubto + `ismainbutsubto` = :ismainbutsubto, + `letsencrypt` = :letsencrypt "); Database::pexecute($ins_stmt, $ins_data); $domainid = Database::lastInsertId(); @@ -1288,6 +1298,11 @@ if ($page == 'domains' $ssl_redirect = (int)$_POST['ssl_redirect']; } + $letsencrypt = 0; + if (isset($_POST['letsencrypt'])) { + $letsencrypt = (int)$_POST['letsencrypt']; + } + $ssl_ipandports = array(); if (isset($_POST['ssl_ipandport']) && !is_array($_POST['ssl_ipandport'])) { $_POST['ssl_ipandport'] = unserialize($_POST['ssl_ipandport']); @@ -1314,12 +1329,14 @@ if ($page == 'domains' } } else { $ssl_redirect = 0; + $letsencrypt = 0; // we need this for the serialize // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; } } else { $ssl_redirect = 0; + $letsencrypt = 0; // we need this for the serialize // if ssl is disabled or no ssl-ip/port exists $ssl_ipandports[] = -1; @@ -1443,7 +1460,8 @@ if ($page == 'domains' 'speciallogfile' => $speciallogfile, 'speciallogverified' => $speciallogverified, 'ipandport' => serialize($ipandports), - 'ssl_ipandport' => serialize($ssl_ipandports) + 'ssl_ipandport' => serialize($ssl_ipandports), + 'letsencrypt' => $letsencrypt ); $security_questions = array( @@ -1478,6 +1496,7 @@ if ($page == 'domains' || $issubof != $result['ismainbutsubto'] || $email_only != $result['email_only'] || ($speciallogfile != $result['speciallogfile'] && $speciallogverified == '1') + || $letsencrypt != $result['letsencrypt'] ) { inserttask('1'); } @@ -1613,6 +1632,7 @@ if ($page == 'domains' $update_data['specialsettings'] = $specialsettings; $update_data['registration_date'] = $registration_date; $update_data['ismainbutsubto'] = $issubof; + $update_data['letsencrypt'] = $letsencrypt; $update_data['id'] = $id; $update_stmt = Database::prepare(" @@ -1638,7 +1658,8 @@ if ($page == 'domains' `mod_fcgid_maxrequests` = :mod_fcgid_maxrequests, `specialsettings` = :specialsettings, `registration_date` = :registration_date, - `ismainbutsubto` = :ismainbutsubto + `ismainbutsubto` = :ismainbutsubto, + `letsencrypt` = :letsencrypt WHERE `id` = :id "); Database::pexecute($update_stmt, $update_data); @@ -1653,9 +1674,10 @@ if ($page == 'domains' // if we have no more ssl-ip's for this domain, // all its subdomains must have "ssl-redirect = 0" + // and disable let's encrypt $update_sslredirect = ''; if (count($ssl_ipandports) == 1 && $ssl_ipandports[0] == -1) { - $update_sslredirect = ", `ssl_redirect` = '0' "; + $update_sslredirect = ", `ssl_redirect` = '0', `letsencrypt` = '0' "; } $_update_stmt = Database::prepare(" diff --git a/lib/formfields/admin/domains/formfield.domains_add.php b/lib/formfields/admin/domains/formfield.domains_add.php index 12dbae4a..1c21af4d 100644 --- a/lib/formfields/admin/domains/formfield.domains_add.php +++ b/lib/formfields/admin/domains/formfield.domains_add.php @@ -113,6 +113,16 @@ return array( ), 'value' => array() ), + 'letsencrypt' => array( + 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), + 'label' => $lng['domains']['letsencrypt']['title'], + 'desc' => $lng['domains']['letsencrypt']['description'], + 'type' => 'checkbox', + 'values' => array( + array ('label' => $lng['panel']['yes'], 'value' => '1') + ), + 'value' => array() + ), 'no_ssl_available_info' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports == '' ? true : false) : false), 'label' => 'SSL', diff --git a/lib/formfields/admin/domains/formfield.domains_edit.php b/lib/formfields/admin/domains/formfield.domains_edit.php index d4d79698..4a77d898 100644 --- a/lib/formfields/admin/domains/formfield.domains_edit.php +++ b/lib/formfields/admin/domains/formfield.domains_edit.php @@ -124,6 +124,16 @@ return array( ), 'value' => array($result['ssl_redirect']) ), + 'letsencrypt' => array( + 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), + 'label' => $lng['domains']['letsencrypt']['title'], + 'desc' => $lng['domains']['letsencrypt']['description'], + 'type' => 'checkbox', + 'values' => array( + array ('label' => $lng['panel']['yes'], 'value' => '1') + ), + 'value' => array($result['letsencrypt']) + ), 'no_ssl_available_info' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports == '' ? true : false) : false), 'label' => 'SSL', From d87fc4c717b4b66a42cbedb378923dfa53f9b6c4 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 14:46:00 +0100 Subject: [PATCH 13/30] Prevent enabling let's encrypt for wildcard - domains Signed-off-by: Florian Aders --- admin_domains.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/admin_domains.php b/admin_domains.php index c897ba01..8bec3e96 100644 --- a/admin_domains.php +++ b/admin_domains.php @@ -565,6 +565,11 @@ if ($page == 'domains' $ssl_ipandports[] = -1; } + // We can't enable let's encrypt for wildcard - domains + if ($serveraliasoption == '0') { + $letsencrypt = 0; + } + if (!preg_match('/^https?\:\/\//', $documentroot)) { if (strstr($documentroot, ":") !== false) { standard_error('pathmaynotcontaincolon'); @@ -1342,6 +1347,11 @@ if ($page == 'domains' $ssl_ipandports[] = -1; } + // We can't enable let's encrypt for wildcard domains + if ($serveraliasoption == '0') { + $letsencrypt = '0'; + } + if (!preg_match('/^https?\:\/\//', $documentroot)) { $documentroot = makeCorrectDir($documentroot); } @@ -1889,6 +1899,7 @@ if ($page == 'domains' $_value = '2'; if ($result['iswildcarddomain'] == '1') { $_value = '0'; + $letsencrypt = 0; } elseif ($result['wwwserveralias'] == '1') { $_value = '1'; } From 272ba3f74e58d75303493110dfcd34eef4e8c6f1 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 14:54:20 +0100 Subject: [PATCH 14/30] Display warning about wildcard - domains with let's encrypt Signed-off-by: Florian Aders --- lng/english.lng.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lng/english.lng.php b/lng/english.lng.php index 4d8c78b3..197f9128 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1927,6 +1927,6 @@ $lng['opcacheinfo']['false'] = 'false'; // Added for let's encrypt $lng['domains']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; -$lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. This certificate will be created and renewed automatically'; +$lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically. This is not possible for wildcard - domains'; $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is only possible when the domain has at least one ssl-enabled IP/port combination assigned.'; $lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; \ No newline at end of file From dd9e540ca3c2918398f5dd749f18de361bea9505 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sat, 30 Jan 2016 15:19:40 +0100 Subject: [PATCH 15/30] inserttask(1) after updating certificates, updated install/update - SQL Signed-off-by: Florian Aders --- install/froxlor.sql | 6 +++--- install/updates/froxlor/0.9/update_0.9.inc.php | 11 +++++------ scripts/jobs/cron_letsencrypt.php | 10 ++++++++++ 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/install/froxlor.sql b/install/froxlor.sql index 09f7f0ac..d968c6e7 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -194,8 +194,8 @@ CREATE TABLE `panel_customers` ( `theme` varchar(255) NOT NULL default 'Sparkle', `custom_notes` text, `custom_notes_show` tinyint(1) NOT NULL default '0', - `lepublickey` text DEFAULT NULL, - `leprivatekey` text DEFAULT NULL, + `lepublickey` mediumtext DEFAULT NULL, + `leprivatekey` mediumtext DEFAULT NULL, PRIMARY KEY (`customerid`), UNIQUE KEY `loginname` (`loginname`) ) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; @@ -249,6 +249,7 @@ CREATE TABLE `panel_domains` ( `mod_fcgid_starter` int(4) default '-1', `mod_fcgid_maxrequests` int(4) default '-1', `ismainbutsubto` int(11) unsigned NOT NULL default '0', + `letsencrypt` tinyint(1) NOT NULL default '0', PRIMARY KEY (`id`), KEY `customerid` (`customerid`), KEY `parentdomain` (`parentdomainid`), @@ -826,7 +827,6 @@ CREATE TABLE IF NOT EXISTS `domain_ssl_settings` ( `ssl_key_file` mediumtext NOT NULL, `ssl_ca_file` mediumtext, `ssl_cert_chainfile` mediumtext, - `letsencrypt` int(11) NOT NULL DEFAULT '0', `expirationdate` datetime DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci; diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index e80708ba..f670d2f5 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3025,12 +3025,11 @@ if (isFroxlorVersion('0.9.34.2')) { showUpdateStep("Updating from 0.9.34.2 to 0.9.35-dev1"); lastStepStatus(0); - showUpdateStep("Adding Let's encrypt - certificate fields"); - Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `letsencrypt` INT NOT NULL DEFAULT '0' AFTER `ssl_cert_chainfile`"); - Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `expirationdate` DATETIME NULL AFTER `letsencrypt`;"); - Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `lepublickey` TEXT DEFAULT NULL AFTER `custom_notes_show`"); - Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `leprivatekey` TEXT DEFAULT NULL AFTER `lepublickey`;"); - Database::query("ALTER TABLE `".TABLE_PANEL_DOMAINS."` ADD `letsencrypt` INT NOT NULL DEFAULT '0' AFTER `ismainbutsubto`;"); + showUpdateStep("Adding Let's Encrypt - certificate fields"); + Database::query("ALTER TABLE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` ADD `expirationdate` DATETIME NULL AFTER `ssl_cert_chainfile`;"); + Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `lepublickey` MEDIUMTEXT DEFAULT NULL AFTER `custom_notes_show`"); + Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` ADD `leprivatekey` MEDIUMTEXT DEFAULT NULL AFTER `lepublickey`;"); + Database::query("ALTER TABLE `".TABLE_PANEL_DOMAINS."` ADD `letsencrypt` TINYINT(1) NOT NULL DEFAULT '0' AFTER `ismainbutsubto`;"); Settings::AddNew("system.leprivatekey", 'unset'); Settings::AddNew("system.lepublickey", 'unset'); lastStepStatus(0); diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index 64772ffa..dae560a4 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -31,6 +31,7 @@ $upd_stmt = Database::prepare(" UPDATE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` SET `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, expirationdate = :expirationdate WHERE `id` = :id "); +$changedetected = 0; while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { // Only renew let's encrypt certificate for domains where a documentroot @@ -81,6 +82,9 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']), 'id' => $certrow['id']) ); + + $changedetected = 1; + } catch (\Exception $e) { fwrite($debugHandler, 'letsencrypt exception: ' . $e->getMessage() . "\n"); } @@ -88,3 +92,9 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { fwrite($debugHandler, 'letsencrypt skipped because documentroot ' . $certrow['documentroot'] . ' does not exist' . "\n"); } } + +// 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) { + inserttask(1); +} \ No newline at end of file From b32c2bb9944772fabdd0271c79394a2bdba3f046 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 11:42:16 +0100 Subject: [PATCH 16/30] Just a small lng - change Signed-off-by: Florian Aders --- lng/english.lng.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lng/english.lng.php b/lng/english.lng.php index 197f9128..b039eb72 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1927,6 +1927,6 @@ $lng['opcacheinfo']['false'] = 'false'; // Added for let's encrypt $lng['domains']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; -$lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically. This is not possible for wildcard - domains'; +$lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:This is not possible for wildcard - domains'; $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is only possible when the domain has at least one ssl-enabled IP/port combination assigned.'; $lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; \ No newline at end of file From b62f4ef911ad81799c496db32edd58d25036f83f Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 11:49:33 +0100 Subject: [PATCH 17/30] Copied ssl_redirect - integritsycheck for lets encrypt Signed-off-by: Florian Aders --- .../integrity/class.IntegrityCheck.php | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/lib/classes/integrity/class.IntegrityCheck.php b/lib/classes/integrity/class.IntegrityCheck.php index bf330760..6d82a594 100644 --- a/lib/classes/integrity/class.IntegrityCheck.php +++ b/lib/classes/integrity/class.IntegrityCheck.php @@ -267,6 +267,76 @@ class IntegrityCheck { } } + /** + * Check if all subdomain have letsencrypt = 0 if domain has no ssl-port + * @param $fix Fix everything found directly + */ + public function SubdomainLetsencrypt($fix = false) { + $ips = array(); + $parentdomains = array(); + $subdomains = array(); + + if ($fix) { + // Prepare update statement for the fixes + $upd_stmt = Database::prepare(" + UPDATE `" . TABLE_PANEL_DOMAINS . "` + SET `letsencrypt` = 0 WHERE `parentdomainid` = :domainid" + ); + } + + // Cache all ssl ip/port - combinations + $result_stmt = Database::prepare("SELECT `id`, `ip`, `port` FROM `" . TABLE_PANEL_IPSANDPORTS . "` WHERE `ssl` = 1 ORDER BY `id` ASC"); + Database::pexecute($result_stmt); + while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { + $ips[$row['id']] = $row['ip'] . ':' . $row['port']; + } + + // Cache all configured domains + $result_stmt = Database::prepare("SELECT `id`, `parentdomainid`, `letsencrypt` FROM `" . TABLE_PANEL_DOMAINS . "` ORDER BY `id` ASC"); + $ip_stmt = Database::prepare("SELECT `id_domain`, `id_ipandports` FROM `" . TABLE_DOMAINTOIP . "` WHERE `id_domain` = :domainid"); + Database::pexecute($result_stmt); + while ($row = $result_stmt->fetch(PDO::FETCH_ASSOC)) { + if ($row['parentdomainid'] == 0) { + // All parentdomains by default have no ssl - ip/port + $parentdomains[$row['id']] = false; + Database::pexecute($ip_stmt, array('domainid' => $row['id'])); + while ($iprow = $ip_stmt->fetch(PDO::FETCH_ASSOC)) { + // If the parentdomain has an ip/port assigned which we know is SSL enabled, set the parentdomain to "true" + if (array_key_exists($iprow['id_ipandports'], $ips)) { $parentdomains[$row['id']] = true; } + } + } elseif ($row['letsencrypt'] == 1) { + // All subdomains with enabled letsencrypt enabled are stored + if (!isset($subdomains[$row['parentdomainid']])) { $subdomains[$row['parentdomainid']] = array(); } + $subdomains[$row['parentdomainid']][] = $row['id']; + } + } + + // Check if every parentdomain with enabled letsencrypt as SSL enabled + foreach ($parentdomains as $id => $sslavailable) { + // This parentdomain has no subdomains + if (!isset($subdomains[$id])) { continue; } + // This parentdomain has SSL enabled, doesn't matter what status the subdomains have + if ($sslavailable) { continue; } + + // At this point only parentdomains reside which have letsencrypt enabled subdomains + if ($fix) { + // We make a blanket update to all subdomains of this parentdomain, doesn't matter which one is wrong, all have to be disabled + Database::pexecute($upd_stmt, array('domainid' => $id)); + $this->_log->logAction(ADM_ACTION, LOG_WARNING, "found a subdomain with letsencrypt=1 but parent-domain has ssl=0, integrity check fixed this"); + } else { + // It's just the check, let the function fail + $this->_log->logAction(ADM_ACTION, LOG_NOTICE, "found a subdomain with letsencrypt=1 but parent-domain has ssl=0, integrity check can fix this"); + return false; + } + } + + if ($fix) { + return $this->SubdomainLetsencrypt(); + } else { + return true; + } + } + /** * check whether the webserveruser is in * the customers groups when fcgid / php-fpm is used From 7814499b87c01c692a5ce200707a25ed1c984129 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 12:09:54 +0100 Subject: [PATCH 18/30] Added cronjob to database and added a hint to the README Signed-off-by: Florian Aders --- README.md | 13 +++++++++++++ install/froxlor.sql | 3 ++- install/updates/froxlor/0.9/update_0.9.inc.php | 11 +++++++++++ lng/english.lng.php | 3 ++- 4 files changed, 28 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ada5c51a..5a24b395 100644 --- a/README.md +++ b/README.md @@ -58,3 +58,16 @@ http://files.froxlor.org/releases/froxlor-latest.tar.gz [MD5](http://files.froxl [HowTo](http://redmine.froxlor.org/projects/froxlor/wiki/Installationgentoo) http://files.froxlor.org/gentoo/repositories.xml + +## Let's encrypt support + +This version of Froxlor contains a beta - version of support for [Let's Encrypt](https://letsencrypt.org). This is (as Let's Encrypt is in itself) +still a test - version and may break your system. The way it currently works is by creating a (sub-)domain with the default system - certificate, +after which the Let's Encrypt cronjob orders the certificate for this (sub-)domain and inserts the certificates in the database. With the next run +of the default cronjob, the certificates will be updated on the disk and the webserver reloaded. + +This has 2 known side-effects at the moment: +* The ip/port combinations don't work with the Froxlor - integration of Let's Encrypt, since it needs a certificate for the very first creation +* After creating a domain, it will have the default certificate for a short time (by default 5 minutes until the cronjob runs the next time) + +It may be possible to fix these issues, but they are not a priority at the moment \ No newline at end of file diff --git a/install/froxlor.sql b/install/froxlor.sql index d968c6e7..6f526b50 100644 --- a/install/froxlor.sql +++ b/install/froxlor.sql @@ -752,7 +752,8 @@ INSERT INTO `cronjobs_run` (`id`, `module`, `cronfile`, `interval`, `isactive`, (3, 'froxlor/ticket', 'used_tickets_reset', '1 DAY', '1', 'cron_ticketsreset'), (4, 'froxlor/ticket', 'ticketarchive', '1 MONTH', '1', 'cron_ticketarchive'), (5, 'froxlor/reports', 'usage_report', '1 DAY', '1', 'cron_usage_report'), - (6, 'froxlor/core', 'mailboxsize', '6 HOUR', '1', 'cron_mailboxsize'); + (6, 'froxlor/core', 'mailboxsize', '6 HOUR', '1', 'cron_mailboxsize'), + (7, 'froxlor/letsencrypt', 'letsencrypt', '5 MINUTE', '1', 'cron_letsencrypt'); diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index f670d2f5..fef33c39 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3032,6 +3032,17 @@ if (isFroxlorVersion('0.9.34.2')) { Database::query("ALTER TABLE `".TABLE_PANEL_DOMAINS."` ADD `letsencrypt` TINYINT(1) NOT NULL DEFAULT '0' AFTER `ismainbutsubto`;"); Settings::AddNew("system.leprivatekey", 'unset'); Settings::AddNew("system.lepublickey", 'unset'); + showUpdateStep("Adding new cron-module for web- and traffic-reporting"); + $stmt = Database::prepare(" + INSERT INTO `" . TABLE_PANEL_CRONRUNS . "` SET + `module` = 'froxlor/letsencrypt', + `cronfile` = 'letsencrypt', + `interval` = '5 MINUTE', + `desc_lng_key` = 'cron_letsencrypt', + `lastrun` = DATE_SUB(NOW(), INTERVAL 5 MINUTE), + `isactive` = 1" + ); + Database::pexecute($stmt); lastStepStatus(0); updateToVersion('0.9.35-dev1'); diff --git a/lng/english.lng.php b/lng/english.lng.php index b039eb72..e24f4f9c 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1929,4 +1929,5 @@ $lng['opcacheinfo']['false'] = 'false'; $lng['domains']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; $lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:This is not possible for wildcard - domains'; $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is only possible when the domain has at least one ssl-enabled IP/port combination assigned.'; -$lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; \ No newline at end of file +$lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; +$lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates'; \ No newline at end of file From e74a4184058d2639c7806beaff0d83474980521c Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 12:13:15 +0100 Subject: [PATCH 19/30] Fixed description Signed-off-by: Florian Aders --- .../updates/froxlor/0.9/update_0.9.inc.php | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/install/updates/froxlor/0.9/update_0.9.inc.php b/install/updates/froxlor/0.9/update_0.9.inc.php index fef33c39..e39aa908 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3032,17 +3032,17 @@ if (isFroxlorVersion('0.9.34.2')) { Database::query("ALTER TABLE `".TABLE_PANEL_DOMAINS."` ADD `letsencrypt` TINYINT(1) NOT NULL DEFAULT '0' AFTER `ismainbutsubto`;"); Settings::AddNew("system.leprivatekey", 'unset'); Settings::AddNew("system.lepublickey", 'unset'); - showUpdateStep("Adding new cron-module for web- and traffic-reporting"); - $stmt = Database::prepare(" - INSERT INTO `" . TABLE_PANEL_CRONRUNS . "` SET - `module` = 'froxlor/letsencrypt', - `cronfile` = 'letsencrypt', - `interval` = '5 MINUTE', - `desc_lng_key` = 'cron_letsencrypt', - `lastrun` = DATE_SUB(NOW(), INTERVAL 5 MINUTE), - `isactive` = 1" - ); - Database::pexecute($stmt); + showUpdateStep("Adding new cron-module for Let's encrypt"); + $stmt = Database::prepare(" + INSERT INTO `" . TABLE_PANEL_CRONRUNS . "` SET + `module` = 'froxlor/letsencrypt', + `cronfile` = 'letsencrypt', + `interval` = '5 MINUTE', + `desc_lng_key` = 'cron_letsencrypt', + `lastrun` = DATE_SUB(NOW(), INTERVAL 5 MINUTE), + `isactive` = 1" + ); + Database::pexecute($stmt); lastStepStatus(0); updateToVersion('0.9.35-dev1'); From c36fb7e8091e314061b0d6e9553d1a1da7ebf125 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 12:21:21 +0100 Subject: [PATCH 20/30] Added description on how to activate the production version of Let's Encrypt Signed-off-by: Florian Aders --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5a24b395..a64d536e 100644 --- a/README.md +++ b/README.md @@ -70,4 +70,7 @@ This has 2 known side-effects at the moment: * The ip/port combinations don't work with the Froxlor - integration of Let's Encrypt, since it needs a certificate for the very first creation * After creating a domain, it will have the default certificate for a short time (by default 5 minutes until the cronjob runs the next time) -It may be possible to fix these issues, but they are not a priority at the moment \ No newline at end of file +It may be possible to fix these issues, but they are not a priority at the moment + +**By default the testing version of Let's Encrypt is used**. To activate the production version, change the `$ca` in `lib/classes/ssl/class.lescript.php` +to `https://acme-v01.api.letsencrypt.org`. \ No newline at end of file From 435edd53f263dcd9f593bc878e727587a57a99fd Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 12:27:00 +0100 Subject: [PATCH 21/30] Minor rewording Signed-off-by: Florian Aders --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a64d536e..1ef04e54 100644 --- a/README.md +++ b/README.md @@ -59,15 +59,15 @@ http://files.froxlor.org/releases/froxlor-latest.tar.gz [MD5](http://files.froxl http://files.froxlor.org/gentoo/repositories.xml -## Let's encrypt support +## Let's Encrypt support -This version of Froxlor contains a beta - version of support for [Let's Encrypt](https://letsencrypt.org). This is (as Let's Encrypt is in itself) -still a test - version and may break your system. The way it currently works is by creating a (sub-)domain with the default system - certificate, +This version of Froxlor contains a test - version of support for [Let's Encrypt](https://letsencrypt.org). This is (as Let's Encrypt is in itself) +still a beta - version and may break your system. The way it currently works is by creating a (sub-)domain with the default system - certificate, after which the Let's Encrypt cronjob orders the certificate for this (sub-)domain and inserts the certificates in the database. With the next run of the default cronjob, the certificates will be updated on the disk and the webserver reloaded. This has 2 known side-effects at the moment: -* The ip/port combinations don't work with the Froxlor - integration of Let's Encrypt, since it needs a certificate for the very first creation +* The basic ip/port combinations don't work with the Froxlor - integration of Let's Encrypt, since it needs a certificate for the very first creation * After creating a domain, it will have the default certificate for a short time (by default 5 minutes until the cronjob runs the next time) It may be possible to fix these issues, but they are not a priority at the moment From 9fc47f55b8e5632804b8f8893042ffee6ab6246f Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Sun, 31 Jan 2016 16:03:10 +0100 Subject: [PATCH 22/30] Minor rewording 2 Signed-off-by: Florian Aders --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1ef04e54..62054376 100644 --- a/README.md +++ b/README.md @@ -61,8 +61,8 @@ http://files.froxlor.org/gentoo/repositories.xml ## Let's Encrypt support -This version of Froxlor contains a test - version of support for [Let's Encrypt](https://letsencrypt.org). This is (as Let's Encrypt is in itself) -still a beta - version and may break your system. The way it currently works is by creating a (sub-)domain with the default system - certificate, +This version of Froxlor contains a test implementation of support for [Let's Encrypt](https://letsencrypt.org). This is (as Let's Encrypt is in itself) +still a beta version and may break your system. The way it currently works is by creating a (sub-)domain with the default system - certificate, after which the Let's Encrypt cronjob orders the certificate for this (sub-)domain and inserts the certificates in the database. With the next run of the default cronjob, the certificates will be updated on the disk and the webserver reloaded. @@ -72,5 +72,5 @@ This has 2 known side-effects at the moment: It may be possible to fix these issues, but they are not a priority at the moment -**By default the testing version of Let's Encrypt is used**. To activate the production version, change the `$ca` in `lib/classes/ssl/class.lescript.php` -to `https://acme-v01.api.letsencrypt.org`. \ No newline at end of file +**By default the testing environment of Let's Encrypt is used**. This issues certificates which will not be signed by a known certificate authority. +To activate the production system, change the `$ca` in `lib/classes/ssl/class.lescript.php` to `https://acme-v01.api.letsencrypt.org`. \ No newline at end of file From daf32b8ac41ce4076880a54fe4d1041c23c2d49c Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Mon, 1 Feb 2016 10:19:31 +0100 Subject: [PATCH 23/30] Froxlor doesn't use namespaces yet :/ Signed-off-by: Florian Aders --- scripts/jobs/cron_letsencrypt.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index dae560a4..2bc75d01 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -85,7 +85,7 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { $changedetected = 1; - } catch (\Exception $e) { + } catch (Exception $e) { fwrite($debugHandler, 'letsencrypt exception: ' . $e->getMessage() . "\n"); } } else { @@ -97,4 +97,4 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { // This is easiest done by just creating a new task ;) if ($changedetected) { inserttask(1); -} \ No newline at end of file +} From eff978e5f6d9ffa629f37d2dd9b532311bbb8529 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Mon, 1 Feb 2016 11:36:20 +0100 Subject: [PATCH 24/30] Using different texts in admin and customer interface Signed-off-by: Florian Aders --- lib/formfields/admin/domains/formfield.domains_add.php | 4 ++-- lib/formfields/admin/domains/formfield.domains_edit.php | 4 ++-- lib/formfields/customer/domains/formfield.domains_add.php | 4 ++-- .../customer/domains/formfield.domains_edit.php | 4 ++-- lng/english.lng.php | 8 +++++--- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/lib/formfields/admin/domains/formfield.domains_add.php b/lib/formfields/admin/domains/formfield.domains_add.php index 1c21af4d..f39c9754 100644 --- a/lib/formfields/admin/domains/formfield.domains_add.php +++ b/lib/formfields/admin/domains/formfield.domains_add.php @@ -115,8 +115,8 @@ return array( ), 'letsencrypt' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), - 'label' => $lng['domains']['letsencrypt']['title'], - 'desc' => $lng['domains']['letsencrypt']['description'], + 'label' => $lng['admin']['letsencrypt']['title'], + 'desc' => $lng['admin']['letsencrypt']['description'], 'type' => 'checkbox', 'values' => array( array ('label' => $lng['panel']['yes'], 'value' => '1') diff --git a/lib/formfields/admin/domains/formfield.domains_edit.php b/lib/formfields/admin/domains/formfield.domains_edit.php index 4a77d898..2c68f208 100644 --- a/lib/formfields/admin/domains/formfield.domains_edit.php +++ b/lib/formfields/admin/domains/formfield.domains_edit.php @@ -126,8 +126,8 @@ return array( ), 'letsencrypt' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), - 'label' => $lng['domains']['letsencrypt']['title'], - 'desc' => $lng['domains']['letsencrypt']['description'], + 'label' => $lng['admin']['letsencrypt']['title'], + 'desc' => $lng['admin']['letsencrypt']['description'], 'type' => 'checkbox', 'values' => array( array ('label' => $lng['panel']['yes'], 'value' => '1') diff --git a/lib/formfields/customer/domains/formfield.domains_add.php b/lib/formfields/customer/domains/formfield.domains_add.php index 856d1bdd..753903ea 100644 --- a/lib/formfields/customer/domains/formfield.domains_add.php +++ b/lib/formfields/customer/domains/formfield.domains_add.php @@ -72,8 +72,8 @@ return array( ), 'letsencrypt' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? true : false) : false), - 'label' => $lng['domains']['letsencrypt']['title'], - 'desc' => $lng['domains']['letsencrypt']['description'], + 'label' => $lng['customer']['letsencrypt']['title'], + 'desc' => $lng['customer']['letsencrypt']['description'], 'type' => 'checkbox', 'values' => array( array ('label' => $lng['panel']['yes'], 'value' => '1') diff --git a/lib/formfields/customer/domains/formfield.domains_edit.php b/lib/formfields/customer/domains/formfield.domains_edit.php index fc501991..abd4905a 100644 --- a/lib/formfields/customer/domains/formfield.domains_edit.php +++ b/lib/formfields/customer/domains/formfield.domains_edit.php @@ -88,8 +88,8 @@ return array( ), 'letsencrypt' => array( 'visible' => (Settings::Get('system.use_ssl') == '1' ? ($ssl_ipsandports != '' ? (domainHasSslIpPort($result['id']) ? true : false) : false) : false), - 'label' => $lng['domains']['letsencrypt']['title'], - 'desc' => $lng['domains']['letsencrypt']['description'], + 'label' => $lng['customer']['letsencrypt']['title'], + 'desc' => $lng['customer']['letsencrypt']['description'], 'type' => 'checkbox', 'values' => array( array ('label' => $lng['panel']['yes'], 'value' => '1') diff --git a/lng/english.lng.php b/lng/english.lng.php index e24f4f9c..80415c77 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1926,8 +1926,10 @@ $lng['opcacheinfo']['true'] = 'true'; $lng['opcacheinfo']['false'] = 'false'; // Added for let's encrypt -$lng['domains']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; -$lng['domains']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:This is not possible for wildcard - domains'; +$lng['admin']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; +$lng['admin']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:This is not possible for wildcard - domains'; +$lng['customer']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; +$lng['customer']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.'; $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is only possible when the domain has at least one ssl-enabled IP/port combination assigned.'; $lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; -$lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates'; \ No newline at end of file +$lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates'; From 6023e65f7dbf1ff1e4e135a91fb52879916265d0 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Mon, 1 Feb 2016 12:55:08 +0100 Subject: [PATCH 25/30] Corrected version Signed-off-by: Florian Aders --- lib/version.inc.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/version.inc.php b/lib/version.inc.php index bc147d13..d7c9fc75 100644 --- a/lib/version.inc.php +++ b/lib/version.inc.php @@ -16,7 +16,7 @@ */ // Main version variable -$version = '0.9.34.2'; +$version = '0.9.35-dev1'; // Database version (unused, old stuff from SysCP) $dbversion = '2'; From ba1181e8ff0dee946ea937c2e51a537ab2cb8b05 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Mon, 1 Feb 2016 20:47:16 +0100 Subject: [PATCH 26/30] Fixed inserting of certificate if it didn't exist yet and return textbased domainkey, not ressource Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 2 +- scripts/jobs/cron_letsencrypt.php | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index b043992b..200dd408 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -241,7 +241,7 @@ class lescript $chain = implode("\n", $certificates); $this->log("Done, returning new certificates and key"); - return array('fullchain' => $fullchain, 'crt' => $crt, 'chain' => $chain, 'key' => $privateDomainKey); + return array('fullchain' => $fullchain, 'crt' => $crt, 'chain' => $chain, 'key' => $domainkey); } private function parsePemFromBody($body) diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index 2bc75d01..19474754 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -21,14 +21,14 @@ fwrite($debugHandler, "updating let's encrypt certificates\n"); $certificates_stmt = Database::query(" - SELECT domssl.`id`, domssl.expirationdate, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, + SELECT domssl.`id`, domssl.`domainid`, domssl.expirationdate, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, dom.`documentroot`, cust.`leprivatekey`, cust.`lepublickey`, cust.customerid FROM `".TABLE_PANEL_CUSTOMERS."` as cust, `".TABLE_PANEL_DOMAINS."` dom LEFT JOIN `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` domssl ON (dom.id = domssl.domainid) WHERE dom.customerid = cust.customerid AND dom.letsencrypt = 1 AND (domssl.expirationdate < DATE_ADD(NOW(), INTERVAL 30 DAY) OR domssl.expirationdate IS NULL) "); $upd_stmt = Database::prepare(" - UPDATE `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` SET `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, expirationdate = :expirationdate WHERE `id` = :id + REPLACE INTO `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` SET `id` = :id, `domainid` = :domainid, `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, expirationdate = :expirationdate "); $changedetected = 0; @@ -76,11 +76,13 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { // Store the new data Database::pexecute($upd_stmt, array( + 'id' => $certrow['id'], + 'domainid' => $certrow['domainid'], 'crt' => $return['crt'], 'key' => $return['key'], 'ca' => $return['fullchain'], - 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']), - 'id' => $certrow['id']) + 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) + ) ); $changedetected = 1; From efc5f37850affdd10124012d67c009bf0c036640 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Mon, 1 Feb 2016 20:58:49 +0100 Subject: [PATCH 27/30] Well, we need a new key if we don't havre one, not if we already have one Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 2 +- scripts/jobs/cron_letsencrypt.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index 200dd408..ab4a30a3 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -182,7 +182,7 @@ class lescript // ---------------------- // generate private key for domain if not exist - if(!is_null($domainkey)) { + if(empty($domainkey)) { $keys = $this->generateKey(); $domainkey = $keys['private']; } diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index 19474754..6bd373a1 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -22,7 +22,7 @@ fwrite($debugHandler, "updating let's encrypt certificates\n"); $certificates_stmt = Database::query(" SELECT domssl.`id`, domssl.`domainid`, domssl.expirationdate, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, - dom.`documentroot`, cust.`leprivatekey`, cust.`lepublickey`, cust.customerid + dom.`documentroot`, dom.`id` as 'domainid', cust.`leprivatekey`, cust.`lepublickey`, cust.customerid FROM `".TABLE_PANEL_CUSTOMERS."` as cust, `".TABLE_PANEL_DOMAINS."` dom LEFT JOIN `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` domssl ON (dom.id = domssl.domainid) WHERE dom.customerid = cust.customerid AND dom.letsencrypt = 1 AND (domssl.expirationdate < DATE_ADD(NOW(), INTERVAL 30 DAY) OR domssl.expirationdate IS NULL) "); From cdb00a76ce6676d7dfc5ef15f4c168be34075750 Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Mon, 1 Feb 2016 21:23:01 +0100 Subject: [PATCH 28/30] Log cert - error in syslog/database Signed-off-by: Florian Aders --- scripts/jobs/cron_letsencrypt.php | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index 6bd373a1..5a847cf6 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -88,6 +88,7 @@ while ($certrow = $certificates_stmt->fetch(PDO::FETCH_ASSOC)) { $changedetected = 1; } catch (Exception $e) { + $cronlog->logAction(CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage()); fwrite($debugHandler, 'letsencrypt exception: ' . $e->getMessage() . "\n"); } } else { From 1814407bfd0f2a77125529fa9b1ee70fc347e1ab Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Tue, 2 Feb 2016 11:17:04 +0100 Subject: [PATCH 29/30] Throw exception if no challenges are returned at all Signed-off-by: Florian Aders --- lib/classes/ssl/class.lescript.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index ab4a30a3..305a3502 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -100,9 +100,13 @@ class lescript array("resource" => "new-authz", "identifier" => array("type" => "dns", "value" => $domain)) ); + if (!array_key_exists('challenges', $response)) { + throw new RuntimeException("No challenges received for $domain. Whole response: ".json_encode($response)); + } + // choose http-01 challange 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)); + 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(); From c9815be0c739ea5b932be23fd7544e0e0b35fdde Mon Sep 17 00:00:00 2001 From: Florian Aders Date: Tue, 2 Feb 2016 22:15:41 +0100 Subject: [PATCH 30/30] German translation and hint that let's enrypt is still beta Signed-off-by: Florian Aders --- lng/english.lng.php | 4 ++-- lng/german.lng.php | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/lng/english.lng.php b/lng/english.lng.php index 80415c77..57864c5a 100644 --- a/lng/english.lng.php +++ b/lng/english.lng.php @@ -1927,9 +1927,9 @@ $lng['opcacheinfo']['false'] = 'false'; // Added for let's encrypt $lng['admin']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; -$lng['admin']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:This is not possible for wildcard - domains'; +$lng['admin']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:This is not possible for wildcard - domains. This feature is still in beta.'; $lng['customer']['letsencrypt']['title'] = 'Use Let\'s Encrypt'; -$lng['customer']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.'; +$lng['customer']['letsencrypt']['description'] = 'Get a free certificate from Let\'s Encrypt. The certificate will be created and renewed automatically.
ATTENTION:"This feature is still in beta.'; $lng['error']['sslredirectonlypossiblewithsslipport'] = 'Using Let\'s Encrypt is only possible when the domain has at least one ssl-enabled IP/port combination assigned.'; $lng['panel']['letsencrypt'] = 'Using Let\'s encrypt'; $lng['crondesc']['cron_letsencrypt'] = 'updating Let\'s Encrypt certificates'; diff --git a/lng/german.lng.php b/lng/german.lng.php index 43213ad3..718ceb18 100644 --- a/lng/german.lng.php +++ b/lng/german.lng.php @@ -1579,3 +1579,12 @@ $lng['admin']['specialsettings_replacements'] = "Die folgenden Variablen können $lng['serversettings']['default_vhostconf']['description'] = 'Der Inhalt dieses Feldes wird direkt in den IP/Port-vHost-Container übernommen. '.$lng['admin']['specialsettings_replacements'].'
ACHTUNG: Der Code wird nicht auf Fehler geprüft. Etwaige Fehler werden also auch übernommen. Der Webserver könnte nicht mehr starten!'; $lng['serversettings']['default_vhostconf_domain']['description'] = 'Der Inhalt dieses Feldes wird direkt in jeden Domain-vHost-Container übernommen. '. $lng['admin']['specialsettings_replacements'].'ACHTUNG: Der Code wird nicht auf Fehler geprüft. Etwaige Fehler werden also auch übernommen. Der Webserver könnte nicht mehr starten!'; $lng['admin']['mod_fcgid_umask']['title'] = 'Umask (Standard: 022)'; + +// Added for let's encrypt +$lng['admin']['letsencrypt']['title'] = 'Benutze Let\'s Encrypt'; +$lng['admin']['letsencrypt']['description'] = 'Holt ein kostenloses Zertifikat von Let\'s Encrypt. Das Zertifikat wird automatisch erstellt und verlänger.
ACHTUNG:Dies ist nicht möglich mit Wildcard - Domains. Dieses Feature befindet sich noch im Test.'; +$lng['customer']['letsencrypt']['title'] = 'Benutze Let\'s Encrypt'; +$lng['customer']['letsencrypt']['description'] = 'Holt ein kostenloses Zertifikat von Let\'s Encrypt. Das Zertifikat wird automatisch erstellt und verlängert.
ACHTUNG:Dieses Feature befindet sich noch im Test.'; +$lng['error']['sslredirectonlypossiblewithsslipport'] = 'Die Nutzung von Let\'s Encrypt ist nur möglich, wenn die Domain mindestens eine IP/Port - Kombination mit aktiviertem SSL zugewiesen hat.'; +$lng['panel']['letsencrypt'] = 'Benutzt Let\'s encrypt'; +$lng['crondesc']['cron_letsencrypt'] = 'aktualisiert Let\'s Encrypt Zertifikate';