From 84f1d94ad63088229497a7d0ed42933ba2c84c7a Mon Sep 17 00:00:00 2001 From: "Michael Kaufmann (d00p)" Date: Mon, 11 Apr 2016 08:02:18 +0200 Subject: [PATCH] check for php-curl installed when cron_letsencrypt runs; format source Signed-off-by: Michael Kaufmann (d00p) --- lib/classes/ssl/class.lescript.php | 784 +++++++++++++++-------------- scripts/jobs/cron_letsencrypt.php | 74 +-- 2 files changed, 442 insertions(+), 416 deletions(-) diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index 32ba7d09..189d2333 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -4,14 +4,14 @@ // // 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. +// * 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 @@ -28,274 +28,285 @@ // and modified to work without files and integrate in Froxlor class lescript { - public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf'; - private $logger; - private $client; - private $accountKey; + public $license = 'https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf'; - public function __construct($logger) - { - $this->logger = $logger; - if (Settings::Get('system.letsencryptca') == 'production') { - $ca = 'https://acme-v01.api.letsencrypt.org'; - } else { - $ca = 'https://acme-staging.api.letsencrypt.org'; - } - $this->client = new Client($ca); - $this->log("Using '$ca' to generate certificate"); - } + private $logger; - public function initAccount($certrow) - { - // Let's see if we have the private accountkey - $this->accountKey = $certrow['leprivatekey']; - if (!$this->accountKey || $this->accountKey == 'unset' || Settings::Get('system.letsencryptca') != 'production') { + private $client; - // generate and save new private key for account - // --------------------------------------------- + private $accountKey; - $this->log('Starting new account registration'); - $keys = $this->generateKey(); - // Only store the accountkey in production, in staging always generate a new key - if (Settings::Get('system.letsencryptca') == 'production') { - $upd_stmt = Database::prepare(" - 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']; - $this->postNewReg(); - $this->log('New account certificate registered'); - - } else { - - $this->log('Account already registered. Continuing.'); - - } - } - - public function signDomains(array $domains, $domainkey = null, $csr = 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); - $accountKeyDetails = openssl_pkey_get_details($privateAccountKey); - - // start domains authentication - // ---------------------------- - - foreach($domains as $domain) { - - // 1. getting available authentication options - // ------------------------------------------- - - $this->log("Requesting challenge for $domain"); - - $response = $this->signedRequest( - "/acme/new-authz", - array("resource" => "new-authz", "identifier" => array("type" => "dns", "value" => $domain)) - ); - - // if response is not an array but a string, it's most likely a server-error, e.g. - // ErrorAn error occurred while processing your request. - //

Reference #179.d8be1402.1458059103.3613c4db - if (!is_array($response)) { - throw new RuntimeException("Invalid response from LE for domain $domain. Whole response: ".$response); - } - - if (!array_key_exists('challenges', $response)) { - throw new RuntimeException("No challenges received for $domain. Whole response: ".json_encode($response)); - } - - // choose http-01 challenge only - $challenge = array_reduce($response['challenges'], function($v, $w) { return $v ? $v : ($w['type'] == 'http-01' ? $w : false); }); - if(!$challenge) throw new RuntimeException("HTTP Challenge for $domain is not available. Whole response: ".json_encode($response)); - - $this->log("Got challenge token for $domain"); - $location = $this->client->getLastLocation(); - - - // 2. saving authentication token for web verification - // --------------------------------------------------- - - $directory = Settings::Get('system.letsencryptchallengepath').'/.well-known/acme-challenge'; - $tokenPath = $directory.'/'.$challenge['token']; - - if(!file_exists($directory) && !@mkdir($directory, 0755, true)) { - throw new \RuntimeException("Couldn't create directory to expose challenge: ${tokenPath}"); - } - - $header = array( - // need to be in precise order! - "e" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["e"]), - "kty" => "RSA", - "n" => Base64UrlSafeEncoder::encode($accountKeyDetails["rsa"]["n"]) - - ); - $payload = $challenge['token'] . '.' . Base64UrlSafeEncoder::encode(hash('sha256', json_encode($header), true)); - - file_put_contents($tokenPath, $payload); - chmod($tokenPath, 0644); - - // 3. verification process itself - // ------------------------------- - - $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - - $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); - - // simple self check - if($payload !== trim(@file_get_contents($uri))) { - $errmsg = json_encode(error_get_last()); - if ($errmsg != "null") { - $errmsg = "; PHP error: " . $errmsg; + public function __construct($logger) + { + $this->logger = $logger; + if (Settings::Get('system.letsencryptca') == 'production') { + $ca = 'https://acme-v01.api.letsencrypt.org'; } else { - $errmsg = ""; + $ca = 'https://acme-staging.api.letsencrypt.org'; } - @unlink($tokenPath); - throw new \RuntimeException("Please check $uri - token not available" . $errmsg); - } + $this->client = new Client($ca); + $this->log("Using '$ca' to generate certificate"); + } - $this->log("Sending request to challenge"); + public function initAccount($certrow) + { + // Let's see if we have the private accountkey + $this->accountKey = $certrow['leprivatekey']; + if (! $this->accountKey || $this->accountKey == 'unset' || Settings::Get('system.letsencryptca') != 'production') { - // send request to challenge - $result = $this->signedRequest( - $challenge['uri'], - array( - "resource" => "challenge", - "type" => "http-01", - "keyAuthorization" => $payload, - "token" => $challenge['token'] - ) - ); + // generate and save new private key for account + // --------------------------------------------- - // waiting loop - // we wait for a maximum of 30 seconds to avoid endless loops - $count = 0; - do { - if(empty($result['status']) || $result['status'] == "invalid") { - @unlink($tokenPath); - throw new \RuntimeException("Verification ended with error: ".json_encode($result)); - } - $ended = !($result['status'] === "pending"); + $this->log('Starting new account registration'); + $keys = $this->generateKey(); + // Only store the accountkey in production, in staging always generate a new key + if (Settings::Get('system.letsencryptca') == 'production') { + $upd_stmt = Database::prepare(" + 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']; + $this->postNewReg(); + $this->log('New account certificate registered'); + } else { - if(!$ended) { - $this->log("Verification pending, sleeping 1s"); - sleep(1); - $count++; - } + $this->log('Account already registered. Continuing.'); + } + } - $result = $this->client->get($location); + public function signDomains(array $domains, $domainkey = null, $csr = null) + { + if (! $this->accountKey) { + throw new \RuntimeException("Account not initiated"); + } - } while (!$ended && $count < 30); + $this->log('Starting certificate generation process for domains'); - $this->log("Verification ended with status: ${result['status']}"); - @unlink($tokenPath); - } + $privateAccountKey = openssl_pkey_get_private($this->accountKey); + $accountKeyDetails = openssl_pkey_get_details($privateAccountKey); - // requesting certificate - // ---------------------- + // start domains authentication + // ---------------------------- - // generate private key for domain if not exist - if(empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) { - $keys = $this->generateKey(); - $domainkey = $keys['private']; - } + foreach ($domains as $domain) { - // load domain key - $privateDomainKey = openssl_pkey_get_private($domainkey); + // 1. getting available authentication options + // ------------------------------------------- - $this->client->getLastLinks(); + $this->log("Requesting challenge for $domain"); - if (empty($csrfile) || Settings::Get('system.letsencryptreuseold') == 0) { - $csr = $this->generateCSR($privateDomainKey, $domains); - } + $response = $this->signedRequest("/acme/new-authz", array( + "resource" => "new-authz", + "identifier" => array( + "type" => "dns", + "value" => $domain + ) + )); - // request certificates creation - $result = $this->signedRequest( - "/acme/new-cert", - array('resource' => 'new-cert', 'csr' => $csr) - ); - if ($this->client->getLastCode() !== 201) { - throw new \RuntimeException("Invalid response code: ".$this->client->getLastCode().", ".json_encode($result)); - } - $location = $this->client->getLastLocation(); + // if response is not an array but a string, it's most likely a server-error, e.g. + // ErrorAn error occurred while processing your request. + //

Reference #179.d8be1402.1458059103.3613c4db + if (! is_array($response)) { + throw new RuntimeException("Invalid response from LE for domain $domain. Whole response: " . $response); + } - // waiting loop - $certificates = array(); - while(1) { - $this->client->getLastLinks(); + if (! array_key_exists('challenges', $response)) { + throw new RuntimeException("No challenges received for $domain. Whole response: " . json_encode($response)); + } - $result = $this->client->get($location); + // choose http-01 challenge only + $challenge = array_reduce($response['challenges'], function ($v, $w) { + return $v ? $v : ($w['type'] == 'http-01' ? $w : false); + }); + if (! $challenge) + throw new RuntimeException("HTTP Challenge for $domain is not available. Whole response: " . json_encode($response)); - if($this->client->getLastCode() == 202) { + $this->log("Got challenge token for $domain"); + $location = $this->client->getLastLocation(); - $this->log("Certificate generation pending, sleeping 1s"); - sleep(1); + // 2. saving authentication token for web verification + // --------------------------------------------------- - } else if ($this->client->getLastCode() == 200) { + $directory = Settings::Get('system.letsencryptchallengepath') . '/.well-known/acme-challenge'; + $tokenPath = $directory . '/' . $challenge['token']; - $this->log("Got certificate! YAY!"); - $certificates[] = $this->parsePemFromBody($result); + 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)); - foreach($this->client->getLastLinks() as $link) { - $this->log("Requesting chained cert at $link"); - $result = $this->client->get($link); - $certificates[] = $this->parsePemFromBody($result); - } + file_put_contents($tokenPath, $payload); + chmod($tokenPath, 0644); - break; - } else { + // 3. verification process itself + // ------------------------------- - throw new \RuntimeException("Can't get certificate: HTTP code ".$this->client->getLastCode()); + $uri = "http://${domain}/.well-known/acme-challenge/${challenge['token']}"; - } - } + $this->log("Token for $domain saved at $tokenPath and should be available at $uri"); - if(empty($certificates)) throw new \RuntimeException('No certificates generated'); + // simple self check + if ($payload !== trim(@file_get_contents($uri))) { + $errmsg = json_encode(error_get_last()); + if ($errmsg != "null") { + $errmsg = "; PHP error: " . $errmsg; + } else { + $errmsg = ""; + } + @unlink($tokenPath); + throw new \RuntimeException("Please check $uri - token not available" . $errmsg); + } - $fullchain = implode("\n", $certificates); - $crt = array_shift($certificates); - $chain = implode("\n", $certificates); + $this->log("Sending request to challenge"); - $this->log("Done, returning new certificates and key"); - return array('fullchain' => $fullchain, 'crt' => $crt, 'chain' => $chain, 'key' => $domainkey, 'csr' => $csr); - } + // send request to challenge + $result = $this->signedRequest($challenge['uri'], array( + "resource" => "challenge", + "type" => "http-01", + "keyAuthorization" => $payload, + "token" => $challenge['token'] + )); - private function parsePemFromBody($body) - { - $pem = chunk_split(base64_encode($body), 64, "\n"); - return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; - } + // waiting loop + // we wait for a maximum of 30 seconds to avoid endless loops + $count = 0; + do { + if (empty($result['status']) || $result['status'] == "invalid") { + @unlink($tokenPath); + throw new \RuntimeException("Verification ended with error: " . json_encode($result)); + } + $ended = ! ($result['status'] === "pending"); - private function postNewReg() - { - $this->log('Sending registration to letsencrypt server'); + if (! $ended) { + $this->log("Verification pending, sleeping 1s"); + sleep(1); + $count ++; + } - return $this->signedRequest( - '/acme/new-reg', - array('resource' => 'new-reg', 'agreement' => $this->license) - ); - } + $result = $this->client->get($location); + } while (! $ended && $count < 30); - 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"]; + $this->log("Verification ended with status: ${result['status']}"); + @unlink($tokenPath); + } - // workaround to get SAN working - fwrite($tmpConf, -'HOME = . + // requesting certificate + // ---------------------- + + // generate private key for domain if not exist + if (empty($domainkey) || Settings::Get('system.letsencryptreuseold') == 0) { + $keys = $this->generateKey(); + $domainkey = $keys['private']; + } + + // load domain key + $privateDomainKey = openssl_pkey_get_private($domainkey); + + $this->client->getLastLinks(); + + if (empty($csrfile) || Settings::Get('system.letsencryptreuseold') == 0) { + $csr = $this->generateCSR($privateDomainKey, $domains); + } + + // request certificates creation + $result = $this->signedRequest("/acme/new-cert", array( + 'resource' => 'new-cert', + 'csr' => $csr + )); + if ($this->client->getLastCode() !== 201) { + throw new \RuntimeException("Invalid response code: " . $this->client->getLastCode() . ", " . json_encode($result)); + } + $location = $this->client->getLastLocation(); + + // waiting loop + $certificates = array(); + while (1) { + $this->client->getLastLinks(); + + $result = $this->client->get($location); + + if ($this->client->getLastCode() == 202) { + + $this->log("Certificate generation pending, sleeping 1s"); + sleep(1); + } 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'); + + $fullchain = implode("\n", $certificates); + $crt = array_shift($certificates); + $chain = implode("\n", $certificates); + + $this->log("Done, returning new certificates and key"); + return array( + 'fullchain' => $fullchain, + 'crt' => $crt, + 'chain' => $chain, + 'key' => $domainkey, + 'csr' => $csr + ); + } + + private function parsePemFromBody($body) + { + $pem = chunk_split(base64_encode($body), 64, "\n"); + return "-----BEGIN CERTIFICATE-----\n" . $pem . "-----END CERTIFICATE-----\n"; + } + + 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 = ' . Settings::Get('system.letsencryptkeysize') . ' @@ -306,197 +317,202 @@ req_extensions = v3_req countryName = Country Name (2 letter code) [ v3_req ] basicConstraints = CA:FALSE -subjectAltName = '.$san.' +subjectAltName = ' . $san . ' keyUsage = nonRepudiation, digitalSignature, keyEncipherment'); - $csr = openssl_csr_new( - array( - "CN" => $domain, - "ST" => Settings::Get('system.letsencryptstate'), - "C" => Settings::Get('system.letsencryptcountrycode'), - "O" => "Unknown", - ), - $privateKey, - array( - "config" => $tmpConfPath, - "digest_alg" => "sha256" - ) - ); + $csr = openssl_csr_new(array( + "CN" => $domain, + "ST" => Settings::Get('system.letsencryptstate'), + "C" => Settings::Get('system.letsencryptcountrycode'), + "O" => "Unknown" + ), $privateKey, array( + "config" => $tmpConfPath, + "digest_alg" => "sha256" + )); - if (!$csr) throw new \RuntimeException("CSR couldn't be generated! ".openssl_error_string()); + if (! $csr) + throw new \RuntimeException("CSR couldn't be generated! " . openssl_error_string()); - openssl_csr_export($csr, $csr); - fclose($tmpConf); + openssl_csr_export($csr, $csr); + fclose($tmpConf); - preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); + preg_match('~REQUEST-----(.*)-----END~s', $csr, $matches); - return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); - } + return trim(Base64UrlSafeEncoder::encode(base64_decode($matches[1]))); + } - private function generateKey() - { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_RSA, - "private_key_bits" => (int)Settings::Get('system.letsencryptkeysize'), - )); + private function generateKey() + { + $res = openssl_pkey_new(array( + "private_key_type" => OPENSSL_KEYTYPE_RSA, + "private_key_bits" => (int) Settings::Get('system.letsencryptkeysize') + )); - if(!openssl_pkey_export($res, $privateKey)) { - throw new \RuntimeException("Key export failed!"); - } + if (! openssl_pkey_export($res, $privateKey)) { + throw new \RuntimeException("Key export failed!"); + } - $details = openssl_pkey_get_details($res); + $details = openssl_pkey_get_details($res); - return array('private' => $privateKey, 'public' => $details['key']); - } + return array( + 'private' => $privateKey, + 'public' => $details['key'] + ); + } - private function signedRequest($uri, array $payload) - { - $privateKey = openssl_pkey_get_private($this->accountKey); - $details = openssl_pkey_get_details($privateKey); + private function signedRequest($uri, array $payload) + { + $privateKey = openssl_pkey_get_private($this->accountKey); + $details = openssl_pkey_get_details($privateKey); - $header = array( - "alg" => "RS256", - "jwk" => array( - "kty" => "RSA", - "n" => Base64UrlSafeEncoder::encode($details["rsa"]["n"]), - "e" => Base64UrlSafeEncoder::encode($details["rsa"]["e"]), - ) - ); + $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(); + $protected = $header; + $protected["nonce"] = $this->client->getLastNonce(); + $payload64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload))); + $protected64 = Base64UrlSafeEncoder::encode(json_encode($protected)); - $payload64 = Base64UrlSafeEncoder::encode(str_replace('\\/', '/', json_encode($payload))); - $protected64 = Base64UrlSafeEncoder::encode(json_encode($protected)); + openssl_sign($protected64 . '.' . $payload64, $signed, $privateKey, "SHA256"); - openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256"); + $signed64 = Base64UrlSafeEncoder::encode($signed); - $signed64 = Base64UrlSafeEncoder::encode($signed); + $data = array( + 'header' => $header, + 'protected' => $protected64, + 'payload' => $payload64, + 'signature' => $signed64 + ); - $data = array( - 'header' => $header, - 'protected' => $protected64, - 'payload' => $payload64, - 'signature' => $signed64 - ); + $this->log("Sending signed request to $uri"); - $this->log("Sending signed request to $uri"); + return $this->client->post($uri, json_encode($data)); + } - return $this->client->post($uri, json_encode($data)); - } - - protected function log($message) - { - $this->logger->logAction(CRON_ACTION, LOG_INFO, "letsencrypt " . $message); - } + protected function log($message) + { + $this->logger->logAction(CRON_ACTION, LOG_INFO, "letsencrypt " . $message); + } } class Client { - private $lastCode; - private $lastHeader; - private $base; + private $lastCode; - public function __construct($base) - { - $this->base = $base; - } + private $lastHeader; - 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); + private $base; - // DO NOT DO THAT! - // curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false); - // curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false); + public function __construct($base) + { + $this->base = $base; + } - switch ($method) { - case 'GET': - break; - case 'POST': - curl_setopt($handle, CURLOPT_POST, true); - curl_setopt($handle, CURLOPT_POSTFIELDS, $data); - break; - } - $response = curl_exec($handle); + 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); - if(curl_errno($handle)) { - throw new \RuntimeException('Curl: '.curl_error($handle)); - } + // DO NOT DO THAT! + // curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, false); + // curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false); - $header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE); + switch ($method) { + case 'GET': + break; + case 'POST': + curl_setopt($handle, CURLOPT_POST, true); + curl_setopt($handle, CURLOPT_POSTFIELDS, $data); + break; + } + $response = curl_exec($handle); - $header = substr($response, 0, $header_size); - $body = substr($response, $header_size); + if (curl_errno($handle)) { + throw new \RuntimeException('Curl: ' . curl_error($handle)); + } - $this->lastHeader = $header; - $this->lastCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); + $header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE); - $data = json_decode($body, true); - return $data === null ? $body : $data; - } + $header = substr($response, 0, $header_size); + $body = substr($response, $header_size); - public function post($url, $data) - { - return $this->curl('POST', $url, $data); - } + $this->lastHeader = $header; + $this->lastCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); - public function get($url) - { - return $this->curl('GET', $url); - } + $data = json_decode($body, true); + return $data === null ? $body : $data; + } - public function getLastNonce() - { - if(preg_match('~Replay\-Nonce: (.+)~i', $this->lastHeader, $matches)) { - return trim($matches[1]); - } + public function post($url, $data) + { + return $this->curl('POST', $url, $data); + } - $this->curl('GET', '/directory'); - return $this->getLastNonce(); - } + public function get($url) + { + return $this->curl('GET', $url); + } - public function getLastLocation() - { - if(preg_match('~Location: (.+)~i', $this->lastHeader, $matches)) { - return trim($matches[1]); - } - return null; - } + public function getLastNonce() + { + if (preg_match('~Replay\-Nonce: (.+)~i', $this->lastHeader, $matches)) { + return trim($matches[1]); + } - public function getLastCode() - { - return $this->lastCode; - } + $this->curl('GET', '/directory'); + return $this->getLastNonce(); + } - public function getLastLinks() - { - preg_match_all('~Link: <(.+)>;rel="up"~', $this->lastHeader, $matches); - return $matches[1]; - } + 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, '-_', '+/')); - } + 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 index 720ae706..1848480f 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -1,4 +1,7 @@ - - * @author Froxlor team (2016-) - * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt - * @package Cron + * @copyright (c) the authors + * @author Florian Aders + * @author Froxlor team (2016-) + * @license GPLv2 http://files.froxlor.org/misc/COPYING.txt + * @package Cron * - * @since 0.9.35 + * @since 0.9.35 * */ $cronlog->logAction(CRON_ACTION, LOG_INFO, "Updating Let's Encrypt certificates"); +if (! extension_loaded('curl')) { + $cronlog->logAction(CRON_ACTION, LOG_ERR, "Let's Encrypt requires the php cURL extension to be installed."); + exit; +} + $certificates_stmt = Database::query(" SELECT domssl.`id`, domssl.`domainid`, domssl.expirationdate, domssl.`ssl_cert_file`, domssl.`ssl_key_file`, domssl.`ssl_ca_file`, domssl.`ssl_csr_file`, dom.`domain`, dom.`iswildcarddomain`, dom.`wwwserveralias`, dom.`documentroot`, dom.`id` as 'domainid', dom.`ssl_redirect`, cust.`leprivatekey`, cust.`lepublickey`, cust.customerid, cust.loginname - FROM `".TABLE_PANEL_CUSTOMERS."` as cust, `".TABLE_PANEL_DOMAINS."` dom LEFT JOIN `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` domssl ON (dom.id = domssl.domainid) + 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) "); $updcert_stmt = Database::prepare(" - REPLACE INTO `".TABLE_PANEL_DOMAIN_SSL_SETTINGS."` SET `id` = :id, `domainid` = :domainid, `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, `ssl_cert_chainfile` = :chain, `ssl_csr_file` = :csr, expirationdate = :expirationdate + REPLACE INTO `" . TABLE_PANEL_DOMAIN_SSL_SETTINGS . "` SET `id` = :id, `domainid` = :domainid, `ssl_cert_file` = :crt, `ssl_key_file` = :key, `ssl_ca_file` = :ca, `ssl_cert_chainfile` = :chain, `ssl_csr_file` = :csr, expirationdate = :expirationdate "); $upddom_stmt = Database::prepare(" - UPDATE `".TABLE_PANEL_DOMAINS."` SET `ssl_redirect` = '1' WHERE `id` = :domainid + UPDATE `" . TABLE_PANEL_DOMAINS . "` SET `ssl_redirect` = '1' WHERE `id` = :domainid "); $changedetected = 0; $certrows = $certificates_stmt->fetchAll(PDO::FETCH_ASSOC); -foreach($certrows AS $certrow) { +foreach ($certrows as $certrow) { - // set logger to corresponding loginname for the log to appear in the users system-log - $cronlog = FroxlorLogger::getInstanceOf(array('loginname' => $certrow['loginname'])); + // set logger to corresponding loginname for the log to appear in the users system-log + $cronlog = FroxlorLogger::getInstanceOf(array( + 'loginname' => $certrow['loginname'] + )); // Only renew let's encrypt certificate if no broken ssl_redirect is enabled - if ($certrow['ssl_redirect'] != 2) - { + if ($certrow['ssl_redirect'] != 2) { $cronlog->logAction(CRON_ACTION, LOG_DEBUG, "Updating " . $certrow['domain']); if ($certrow['ssl_cert_file']) { @@ -55,12 +64,14 @@ foreach($certrows AS $certrow) { // We are interessted in the old SAN - data $san = explode(', ', $x509data['extensions']['subjectAltName']); $domains = array(); - foreach($san as $dnsname) { + foreach ($san as $dnsname) { $domains[] = substr($dnsname, 4); } } else { $cronlog->logAction(CRON_ACTION, LOG_DEBUG, "letsencrypt generating new key / SAN for " . $certrow['domain']); - $domains = array($certrow['domain']); + $domains = array( + $certrow['domain'] + ); // Add www. for SAN if ($certrow['wwwserveralias'] == 1) { $domains[] = 'www.' . $certrow['domain']; @@ -82,28 +93,25 @@ foreach($certrows AS $certrow) { // Store the new data Database::pexecute($updcert_stmt, array( - 'id' => $certrow['id'], - 'domainid' => $certrow['domainid'], - 'crt' => $return['crt'], - 'key' => $return['key'], - 'ca' => $return['chain'], - 'chain' => $return['chain'], - 'csr' => $return['csr'], - 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) - ) - ); + 'id' => $certrow['id'], + 'domainid' => $certrow['domainid'], + 'crt' => $return['crt'], + 'key' => $return['key'], + 'ca' => $return['chain'], + 'chain' => $return['chain'], + 'csr' => $return['csr'], + 'expirationdate' => date('Y-m-d H:i:s', $newcert['validTo_time_t']) + )); if ($certrow['ssl_redirect'] == 3) { Database::pexecute($upddom_stmt, array( - 'domainid' => $certrow['domainid'] - ) - ); + 'domainid' => $certrow['domainid'] + )); } $cronlog->logAction(CRON_ACTION, LOG_INFO, "Updated Let's Encrypt certificate for " . $certrow['domain']); $changedetected = 1; - } catch (Exception $e) { $cronlog->logAction(CRON_ACTION, LOG_ERR, "Could not get Let's Encrypt certificate for " . $certrow['domain'] . ": " . $e->getMessage()); } @@ -119,5 +127,7 @@ if ($changedetected) { } // reset logger -$cronlog = FroxlorLogger::getInstanceOf(array('loginname' => 'cronjob')); +$cronlog = FroxlorLogger::getInstanceOf(array( + 'loginname' => 'cronjob' +)); $cronlog->logAction(CRON_ACTION, LOG_INFO, "Let's Encrypt certificates have been updated");