Initial version of let's encrypt renewal cron
Signed-off-by: Florian Aders <eleras@froxlor.org>
This commit is contained in:
@@ -509,6 +509,8 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES
|
|||||||
('system', 'dns_createhostnameentry', '0'),
|
('system', 'dns_createhostnameentry', '0'),
|
||||||
('system', 'send_cron_errors', '0'),
|
('system', 'send_cron_errors', '0'),
|
||||||
('system', 'apacheitksupport', '0'),
|
('system', 'apacheitksupport', '0'),
|
||||||
|
('system', 'leprivatekey', 'unset'),
|
||||||
|
('system', 'lepublickey', 'unset'),
|
||||||
('panel', 'decimal_places', '4'),
|
('panel', 'decimal_places', '4'),
|
||||||
('panel', 'adminmail', 'admin@SERVERNAME'),
|
('panel', 'adminmail', 'admin@SERVERNAME'),
|
||||||
('panel', 'phpmyadmin_url', ''),
|
('panel', 'phpmyadmin_url', ''),
|
||||||
@@ -539,7 +541,7 @@ INSERT INTO `panel_settings` (`settinggroup`, `varname`, `value`) VALUES
|
|||||||
('panel', 'password_numeric', '0'),
|
('panel', 'password_numeric', '0'),
|
||||||
('panel', 'password_special_char_required', '0'),
|
('panel', 'password_special_char_required', '0'),
|
||||||
('panel', 'password_special_char', '!?<>§$%+#=@'),
|
('panel', 'password_special_char', '!?<>§$%+#=@'),
|
||||||
('panel', 'version', '0.9.34.2');
|
('panel', 'version', '0.9.35-dev1');
|
||||||
|
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `panel_tasks`;
|
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_key_file` mediumtext NOT NULL,
|
||||||
`ssl_ca_file` mediumtext,
|
`ssl_ca_file` mediumtext,
|
||||||
`ssl_cert_chainfile` mediumtext,
|
`ssl_cert_chainfile` mediumtext,
|
||||||
|
`letsencrypt` int(11) NOT NULL DEFAULT '0',
|
||||||
|
`expirationdate` datetime DEFAULT NULL
|
||||||
PRIMARY KEY (`id`)
|
PRIMARY KEY (`id`)
|
||||||
) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci;
|
) ENGINE=MyISAM CHARSET=utf8 COLLATE=utf8_general_ci;
|
||||||
|
|
||||||
|
|||||||
@@ -3020,3 +3020,18 @@ if (isFroxlorVersion('0.9.34.1')) {
|
|||||||
|
|
||||||
updateToVersion('0.9.34.2');
|
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');
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
477
lib/classes/ssl/class.lescript.php
Normal file
477
lib/classes/ssl/class.lescript.php
Normal file
@@ -0,0 +1,477 @@
|
|||||||
|
<?php
|
||||||
|
# Copyright (c) 2015, Stanislav Humplik <sh@analogic.cz>
|
||||||
|
# All rights reserved.
|
||||||
|
#
|
||||||
|
# Redistribution and use in source and binary forms, with or without
|
||||||
|
# modification, are permitted provided that the following conditions are met:
|
||||||
|
# * Redistributions of source code must retain the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer.
|
||||||
|
# * Redistributions in binary form must reproduce the above copyright
|
||||||
|
# notice, this list of conditions and the following disclaimer in the
|
||||||
|
# documentation and/or other materials provided with the distribution.
|
||||||
|
# * Neither the name of the <organization> nor the
|
||||||
|
# names of its contributors may be used to endorse or promote products
|
||||||
|
# derived from this software without specific prior written permission.
|
||||||
|
#
|
||||||
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||||
|
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||||
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||||
|
# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
|
||||||
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||||
|
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||||
|
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||||
|
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||||
|
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
|
|
||||||
|
# This file is copied from https://github.com/analogic/lescript
|
||||||
|
# and modified to work without files and integrate in Froxlor
|
||||||
|
class lescript
|
||||||
|
{
|
||||||
|
#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, '-_', '+/'));
|
||||||
|
}
|
||||||
|
}
|
||||||
73
scripts/jobs/cron_letsencrypt.php
Normal file
73
scripts/jobs/cron_letsencrypt.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?php if (!defined('MASTER_CRONJOB')) die('You cannot access this file directly!');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This file is part of the Froxlor project.
|
||||||
|
* Copyright (c) 2016 the Froxlor Team (see authors).
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the COPYING
|
||||||
|
* file that was distributed with this source code. You can also view the
|
||||||
|
* COPYING file online at http://files.froxlor.org/misc/COPYING.txt
|
||||||
|
*
|
||||||
|
* @copyright (c) the authors
|
||||||
|
* @author Florian Aders <kontakt-froxlor@neteraser.de>
|
||||||
|
* @author Froxlor team <team@froxlor.org> (2016-)
|
||||||
|
* @license GPLv2 http://files.froxlor.org/misc/COPYING.txt
|
||||||
|
* @package Cron
|
||||||
|
*
|
||||||
|
* @since 0.9.35
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user