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 8fb02c7a..3fd04142 100644 --- a/install/updates/froxlor/0.9/update_0.9.inc.php +++ b/install/updates/froxlor/0.9/update_0.9.inc.php @@ -3546,3 +3546,29 @@ if (isFroxlorVersion('0.9.38.2')) { showUpdateStep("Updating from 0.9.38.2 to 0.9.38.3", false); updateToVersion('0.9.38.3'); } + +if (isDatabaseVersion('201611180')) { + + showUpdateStep("Adding field to reflect let's-encrypt registration status"); + Database::query("ALTER TABLE `".TABLE_PANEL_CUSTOMERS."` add `leregistered` TINYINT(1) NOT NULL DEFAULT 0;"); + lastStepStatus(0); + + updateToDbVersion('201611240'); +} + +if (isDatabaseVersion('201611240')) { + + showUpdateStep("Adding new setting to reflect let's-encrypt registration status"); + $stmt = Database::prepare(" + INSERT INTO `" . TABLE_PANEL_SETTINGS . "` SET + `settinggroup` = 'system', + `varname` = :varname, + `value` = :value"); + Database::pexecute($stmt, array( + 'varname' => 'leregistered', + 'value' => '0' + )); + lastStepStatus(0); + + updateToDbVersion('201611241'); +} diff --git a/lib/classes/ssl/class.lescript.php b/lib/classes/ssl/class.lescript.php index dc8ec83b..1d3aa5bb 100644 --- a/lib/classes/ssl/class.lescript.php +++ b/lib/classes/ssl/class.lescript.php @@ -38,6 +38,12 @@ class lescript private $accountKey; + private $customerid; + + private $isFroxlorVhost; + + private $isLeProduction; + private $version; public function __construct($logger, $version = '1') @@ -57,44 +63,71 @@ class lescript { // Let's see if we have the private accountkey $this->accountKey = $certrow['leprivatekey']; - if (! $this->accountKey || $this->accountKey == 'unset' || Settings::Get('system.letsencryptca') != 'production') { + $this->customerId = $certrow['customerid']; + $this->isFroxlorVhost = $isFroxlorVhost; + $this->isLeProduction = (Settings::Get('system.letsencryptca') == 'production'); + + $leregistered=$certrow['leregistered']; + + if (! $this->accountKey || $this->accountKey == 'unset' || !$this->isLeProduction) { // generate and save new private key for account // --------------------------------------------- - $this->log('Starting new account registration'); + $this->log('Creating new account key'); $keys = $this->generateKey(); // Only store the accountkey in production, in staging always generate a new key - if (Settings::Get('system.letsencryptca') == 'production') { + if ($this->isLeProduction) { if ($isFroxlorVhost) { Settings::Set('system.lepublickey', $keys['public']); Settings::Set('system.leprivatekey', $keys['private']); + Settings::Set('system.leregistered', 0); // key is not registered } else { - $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private " . "WHERE `customerid` = :customerid;"); + $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `lepublickey` = :public, `leprivatekey` = :private, `leregistered` = :registered " . "WHERE `customerid` = :customerid;"); Database::pexecute($upd_stmt, array( 'public' => $keys['public'], 'private' => $keys['private'], - 'customerid' => $certrow['customerid'] + 'registered' => 0, + 'customerid' => $this->customerId )); } } + $leregistered=0; $this->accountKey = $keys['private']; + } else { + $this->log('Using existing account key'); + } + if ($leregistered==0) { // Account not registered + + $this->log('Starting new account registration'); $response = $this->postNewReg(); - if ($this->client->getLastCode() != 201) { + if ($this->client->getLastCode() == 409) { + $this->log('The key was already registered. Using existing account.'); + } else if ($this->client->getLastCode() == 201) { + $this->log('New account registered.'); + } else { throw new \RuntimeException("Account not initialized, probably due to rate limiting. Whole response: " . json_encode($response)); } + $accountUrl=$this->client->getLastLocation(); + + $this->log('Accepting lets encrypt Terms of Service'); + $this->license = $this->client->getAgreementURL(); - // Terms of Servce are optional according to ACME specs; if no ToS are presented, no need to update registration + // Terms of Service are optional according to ACME specs; if no ToS are presented, no need to update registration if (!empty($this->license)) { - $this->postRegAgreement(parse_url($this->client->getLastLocation(), PHP_URL_PATH)); + $response = $this->postRegAgreement(parse_url($accountUrl, PHP_URL_PATH)); + if ($this->client->getLastCode() != 202) { + throw new \RuntimeException("Terms of Service not accepted. Whole response: " . json_encode($response)); + } } - $this->log('New account certificate registered'); - } else { - $this->log('Account already registered. Continuing.'); + $leregistered=1; + $this->setLeRegisteredState($leregistered); // Account registered + $this->log('Lets encrypt Terms of Service accepted'); } + } /** @@ -136,11 +169,17 @@ class lescript ) )); + if ($this->client->getLastCode() == 403) { + $this->log("Got status 403 - setting LE status to unregistered."); + $this->setLeRegisteredState(0); + throw new RuntimeException("Got 'unauthorized' response - we need to re-register at next run. Whole response: " . json_encode($response)); + } + // if response is not an array but a string, it's most likely a server-error, e.g. // 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); + throw new RuntimeException("Invalid response from LE for domain $domain. Whole response: " . json_encode($response)); } if (! array_key_exists('challenges', $response)) { @@ -309,6 +348,21 @@ class lescript ); } + private function setLeRegisteredState($state) + { + if ($this->isLeProduction) { + if ($this->isFroxlorVhost) { + Settings::Set('system.leregistered', $state); + } else { + $upd_stmt = Database::prepare("UPDATE `" . TABLE_PANEL_CUSTOMERS . "` SET `leregistered` = :registered " . "WHERE `customerid` = :customerid;"); + Database::pexecute($upd_stmt, array( + 'registered' => $state, + 'customerid' => $this->customerId + )); + } + } + } + private function parsePemFromBody($body) { $pem = chunk_split(base64_encode($body), 64, "\n"); @@ -537,10 +591,46 @@ class Client return $matches[1]; } + public function getAgreementURLFromLastResponse() + { + if (preg_match_all('~Link: <(.+)>;rel="terms-of-service"~', $this->lastHeader, $matches)) { + return $matches[1][0]; + } + return ""; + } + public function getAgreementURLFromDirectory() + { + // FIXME: Current license should be found in /directory but LE does not implement this yet + // $this->curl('GET', '/directory'); + return ""; + } + public function getAgreementURLFromTermsUrl() + { + $this->curl('GET', '/terms'); + if (preg_match_all('~Location: (.+)~', $this->lastHeader, $matches)) { + return trim($matches[1][0]); + } + return ""; + } + public function getAgreementURL() { - preg_match_all('~Link: <(.+)>;rel="terms-of-service"~', $this->lastHeader, $matches); - return $matches[1][0]; + // 1. check the header of the last response + $license=$this->getAgreementURLFromLastResponse(); + if (!empty($license)) return $license; + + // 2. query directory for license + $license=$this->getAgreementURLFromDirectory(); + if (!empty($license)) return $license; + + // 3. query /terms endpoint (not ACME standard but implemented by let's enrypt) + $license=$this->getAgreementURLFromTermsUrl(); + if (!empty($license)) return $license; + + // Fallback: use latest known license. This is only valid for let's encrypt and should be removed as soon as there is an official + // ACME-endpoint to get the current ToS + return "xxxhttps://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf]"; + // return ""; } } diff --git a/lib/version.inc.php b/lib/version.inc.php index 915532b7..056ce6e4 100644 --- a/lib/version.inc.php +++ b/lib/version.inc.php @@ -19,7 +19,7 @@ $version = '0.9.38.3'; // Database version (YYYYMMDDC where C is a daily counter) -$dbversion = '201611180'; +$dbversion = '201611241'; // Distribution branding-tag (used for Debian etc.) $branding = ''; diff --git a/scripts/jobs/cron_letsencrypt.php b/scripts/jobs/cron_letsencrypt.php index 2101fcca..126d7dcd 100644 --- a/scripts/jobs/cron_letsencrypt.php +++ b/scripts/jobs/cron_letsencrypt.php @@ -43,6 +43,7 @@ $certificates_stmt = Database::query(" dom.`ssl_redirect`, cust.`leprivatekey`, cust.`lepublickey`, + cust.`leregistered`, cust.`customerid`, cust.`loginname` FROM @@ -103,6 +104,7 @@ if (Settings::Get('system.le_froxlor_enabled') == '1') { 'documentroot' => FROXLOR_INSTALL_DIR, 'leprivatekey' => Settings::Get('system.leprivatekey'), 'lepublickey' => Settings::Get('system.lepublickey'), + 'leregistered' => Settings::Get('system.leregistered'), 'ssl_redirect' => Settings::Get('system.le_froxlor_redirect'), 'expirationdate' => null, 'ssl_cert_file' => null,