From 08895d1a851c688a99ace73021dd3923f3c44c7a Mon Sep 17 00:00:00 2001 From: yourivw Date: Fri, 27 Mar 2020 23:11:14 +0100 Subject: [PATCH] Update to version 1.2.0 Version 1.2.0: - Added custom exceptions per class, with multiple specific exception codes per exception. All exceptions are extended from LEException, which is extended from RuntimeException, and therefore backwards compatible. Response data (request, header, status, body) is added when LEConnectorException::InvalidResponseException is thrown. - All GET requests, except from the initial /directory request, are changed to POST-as-GET requests to comply with ACME changes. This also resolves current related issues when using the staging endpoint. - Bugfix in LEAccount->updateAccount(), implemented isset check for $post['body']['id'] similair to LEAccount->getLEAccountData(). - Bugfix in LEAccount->deactivateAccount(), now returns true after deactivation. - Change in LEOrder setup parameter check (notBefore and notAfter). - Example code endpoint changed to staging endpoint. - All code has been extensively tested again. --- README.md | 30 ++--- examples/dns_finish.php | 2 +- examples/dns_init.php | 2 +- examples/http.php | 2 +- src/Exceptions/LEAccountException.php | 46 +++++++ src/Exceptions/LEAuthorizationException.php | 46 +++++++ src/Exceptions/LEClientException.php | 52 ++++++++ src/Exceptions/LEConnectorException.php | 71 ++++++++++ src/Exceptions/LEException.php | 52 ++++++++ src/Exceptions/LEFunctionsException.php | 58 ++++++++ src/Exceptions/LEOrderException.php | 70 ++++++++++ src/LEAccount.php | 10 +- src/LEAuthorization.php | 30 +++-- src/LEClient.php | 26 ++-- src/LEConnector.php | 24 ++-- src/LEFunctions.php | 33 +++-- src/LEOrder.php | 138 ++++++++++---------- 17 files changed, 548 insertions(+), 144 deletions(-) create mode 100644 src/Exceptions/LEAccountException.php create mode 100644 src/Exceptions/LEAuthorizationException.php create mode 100644 src/Exceptions/LEClientException.php create mode 100644 src/Exceptions/LEConnectorException.php create mode 100644 src/Exceptions/LEException.php create mode 100644 src/Exceptions/LEFunctionsException.php create mode 100644 src/Exceptions/LEOrderException.php diff --git a/README.md b/README.md index 4c92ae3..64af0d4 100644 --- a/README.md +++ b/README.md @@ -3,11 +3,7 @@ PHP LetsEncrypt client library for ACME v2. The aim of this client is to make an ## Current version -The current version is 1.1.11 - -The example codes below are to be updated. - -This client was developed with the use of the LetsEncrypt staging server for version 2. While version 2 is still being developed and implemented by LetsEncrypt at this moment, the project might be subject to change. +The current version is 1.2.0 ## Getting Started @@ -17,7 +13,9 @@ Also have a look at the [LetsEncrypt documentation](https://letsencrypt.org/docs ### Prerequisites -The minimum required PHP version is 5.2.0. Version 7.1.0 is required for EC keys. The function generating EC keys will throw an exception when trying to generate EC keys with a PHP version below 7.1.0. Version 1.0.0 will be kept available, but will not be maintained. +The minimum required PHP version is 5.2.0. Version 7.1.0 is required for EC keys. The function generating EC keys will throw an exception when trying to generate EC keys with a PHP version below 7.1.0. + +Version 1.0.0 will be kept available, but will not be maintained. This client also depends on cURL and OpenSSL. @@ -28,9 +26,7 @@ Using composer: composer require yourivw/leclient ``` -Although it is possible to add this to your own autoloader, it's not recommended as you'll have no control of the dependencies. If you haven't used composer before, I strongly recommend you check it out at [https://getcomposer.org](https://getcomposer.org). - -It is advisable to cut the script some slack regarding execution time by setting a higher maximum time. There are several ways to do so. One it to add the following to the top of the page: +It is advisable to cut the script some slack regarding execution time by setting a higher maximum time. There are several ways to do so. One is to add the following to the top of the page: ```php ini_set('max_execution_time', 120); // Maximum execution time in seconds. ``` @@ -47,11 +43,13 @@ Initiating the client: ```php use LEClient\LEClient; -$client = new LEClient($email); // Initiating a basic LEClient with an array of string e-mail address(es). -$client = new LEClient($email, true); // Initiating a LECLient and use the LetsEncrypt staging URL. -$client = new LEClient($email, true, $logger); // Initiating a LEClient and use a PSR-3 logger (\Psr\Log\LoggerInterface). -$client = new LEClient($email, true, LEClient::LOG_STATUS); // Initiating a LEClient and log status messages (LOG_DEBUG for full debugging). -$client = new LEClient($email, true, LEClient::LOG_STATUS, 'keys/'); // Initiating a LEClient and select custom certificate keys directory (string or array) +$client = new LEClient($email); // Initiating a basic LEClient with an array of string e-mail address(es). +$client = new LEClient($email, LEClient::LE_STAGING); // Initiating a LECLient and use the LetsEncrypt staging URL. +$client = new LEClient($email, LEClient::LE_PRODUCTION); // Initiating a LECLient and use the LetsEncrypt production URL. +$client = new LEClient($email, true); // Initiating a LECLient and use the LetsEncrypt staging URL. +$client = new LEClient($email, true, $logger); // Initiating a LEClient and use a PSR-3 logger (\Psr\Log\LoggerInterface). +$client = new LEClient($email, true, LEClient::LOG_STATUS); // Initiating a LEClient and log status messages (LOG_DEBUG for full debugging). +$client = new LEClient($email, true, LEClient::LOG_STATUS, 'keys/'); // Initiating a LEClient and select custom certificate keys directory (string or array) $client = new LEClient($email, true, LEClient::LOG_STATUS, 'keys/', '__account/'); // Initiating a LEClient and select custom account keys directory (string or array) ``` The client will automatically create a new account if there isn't one found. It will forward the e-mail address(es) supplied during initiation, as shown above. @@ -78,6 +76,8 @@ $order = $client->getOrCreateOrder($basename, $domains, $keyType, $notBefore, $n Using the order functions: ```php +use LEClient\LEOrder; + $valid = $order->allAuthorizationsValid(); // Check whether all authorizations in this order instance are valid. $pending = $order->getPendingAuthorizations($type); // Get an array of pending authorizations. Performing authorizations is described further on. Type is LEOrder::CHALLENGE_TYPE_HTTP or LEOrder::CHALLENGE_TYPE_DNS. $verify = $order->verifyPendingOrderAuthorization($identifier, $type); // Verify a pending order. The identifier is a string domain name. Type is LEOrder::CHALLENGE_TYPE_HTTP or LEOrder::CHALLENGE_TYPE_DNS. @@ -176,7 +176,7 @@ The DNS record name also depends on your provider, therefore getPendingAuthoriza For both HTTP and DNS authorizations, a full example is available in the project's main code directory. The HTTP authorization example is contained in one file. As described above, the DNS authorization example is split into two parts, to allow for the DNS record to update in the meantime. While the TTL of the record might be low, it can sometimes take some time for your provider to update your DNS records after an amendment. -If you can't get these examples, or the client library to work, try and have a look at the LetsEncrypt documentation mentioned above as well. +If you can't get these examples, or the client library to work, try and have a look at the LetsEncrypt documentation mentioned above as well. In order for the example code to work, make sure to replace all 'example.org' information with your own information. The examples will fail when you run them using the preset example data. ## Security diff --git a/examples/dns_finish.php b/examples/dns_finish.php index f9071b8..ee1b91d 100644 --- a/examples/dns_finish.php +++ b/examples/dns_finish.php @@ -16,7 +16,7 @@ $domains = array('example.org', 'test.example.org'); // Initiating the client instance. In this case using the staging server (argument 2) and outputting all status and debug information (argument 3). -$client = new LEClient($email, true, LECLient::LOG_STATUS); +$client = new LEClient($email, LEClient::LE_STAGING, LECLient::LOG_STATUS); // Initiating the order instance. The keys and certificate will be stored in /example.org/ (argument 1) and the domains in the array (argument 2) will be on the certificate. $order = $client->getOrCreateOrder($basename, $domains); // Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations. diff --git a/examples/dns_init.php b/examples/dns_init.php index b4b6396..a643718 100644 --- a/examples/dns_init.php +++ b/examples/dns_init.php @@ -16,7 +16,7 @@ $domains = array('example.org', 'test.example.org'); // Initiating the client instance. In this case using the staging server (argument 2) and outputting all status and debug information (argument 3). -$client = new LEClient($email, true, LECLient::LOG_STATUS); +$client = new LEClient($email, LEClient::LE_STAGING, LECLient::LOG_STATUS); // Initiating the order instance. The keys and certificate will be stored in /example.org/ (argument 1) and the domains in the array (argument 2) will be on the certificate. $order = $client->getOrCreateOrder($basename, $domains); // Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations. diff --git a/examples/http.php b/examples/http.php index 45f2055..9f5c899 100644 --- a/examples/http.php +++ b/examples/http.php @@ -16,7 +16,7 @@ $domains = array('example.org', 'test.example.org'); // Initiating the client instance. In this case using the staging server (argument 2) and outputting all status and debug information (argument 3). -$client = new LEClient($email, true, LECLient::LOG_STATUS); +$client = new LEClient($email, LEClient::LE_STAGING, LECLient::LOG_STATUS); // Initiating the order instance. The keys and certificate will be stored in /example.org/ (argument 1) and the domains in the array (argument 2) will be on the certificate. $order = $client->getOrCreateOrder($basename, $domains); // Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations. diff --git a/src/Exceptions/LEAccountException.php b/src/Exceptions/LEAccountException.php new file mode 100644 index 0000000..1caf370 --- /dev/null +++ b/src/Exceptions/LEAccountException.php @@ -0,0 +1,46 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEAccountException extends LEException +{ + public const ACCOUNTNOTFOUNDEEXCEPTION = 0x21; + + public static function AccountNotFoundException() + { + return new static('Account not found or deactivated.', self::ACCOUNTNOTFOUNDEEXCEPTION); + } +} diff --git a/src/Exceptions/LEAuthorizationException.php b/src/Exceptions/LEAuthorizationException.php new file mode 100644 index 0000000..51a0c45 --- /dev/null +++ b/src/Exceptions/LEAuthorizationException.php @@ -0,0 +1,46 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEAuthorizationException extends LEException +{ + public const NOCHALLENGEFOUNDEEXCEPTION = 0x41; + + public static function NoChallengeFoundException($type, $identifier) + { + return new static(sprintf('No challenge found for type \'%s\' and identifier \'%s\'.', $type, $identifier), self::NOCHALLENGEFOUNDEEXCEPTION); + } +} diff --git a/src/Exceptions/LEClientException.php b/src/Exceptions/LEClientException.php new file mode 100644 index 0000000..24a8c72 --- /dev/null +++ b/src/Exceptions/LEClientException.php @@ -0,0 +1,52 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEClientException extends LEException +{ + public const INVALIDARGUMENTEXCEPTION = 0x01; + public const INVALIDDIRECTORYEXCEPTION = 0x02; + + public static function InvalidArgumentException(string $message) + { + return new static($message, self::INVALIDARGUMENTEXCEPTION); + } + + public static function InvalidDirectoryException(string $directory) + { + return new static(sprintf('%s directory not found.', $directory), self::INVALIDDIRECTORYEXCEPTION); + } +} diff --git a/src/Exceptions/LEConnectorException.php b/src/Exceptions/LEConnectorException.php new file mode 100644 index 0000000..ff34e8a --- /dev/null +++ b/src/Exceptions/LEConnectorException.php @@ -0,0 +1,71 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEConnectorException extends LEException +{ + public const NONEWNONCEEXCEPTION = 0x11; + public const ACCOUNTDEACTIVATEDEXCEPTION = 0x12; + public const METHODNOTSUPPORTEDEXCEPTION = 0x13; + public const CURLERROREXCEPTION = 0x14; + public const INVALIDRESPONSEEXCEPTION = 0x15; + + public static function NoNewNonceException() + { + return new static('No new nonce.', self::NONEWNONCEEXCEPTION); + } + + public static function AccountDeactivatedException() + { + return new static('The account was deactivated. No further requests can be made.', self::ACCOUNTDEACTIVATEDEXCEPTION); + } + + public static function MethodNotSupportedException(string $method) + { + return new static(sprintf('HTTP request %s not supported.', $method), self::METHODNOTSUPPORTEDEXCEPTION); + } + + public static function CurlErrorException(string $error) + { + return new static(sprintf('Curl error: %s', $error), self::CURLERROREXCEPTION); + } + + public static function InvalidResponseException(array $response) + { + $statusCode = array_key_exists('status', $response) ? $response['status'] : 'unknown'; + return new static(sprintf('Invalid response: %s', $statusCode), self::INVALIDRESPONSEEXCEPTION, null, $response); + } +} diff --git a/src/Exceptions/LEException.php b/src/Exceptions/LEException.php new file mode 100644 index 0000000..3c0385c --- /dev/null +++ b/src/Exceptions/LEException.php @@ -0,0 +1,52 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEException extends \RuntimeException +{ + protected $responsedata; + + public function __construct(string $message = "", int $code = 0, Throwable $previous = NULL, array $responsedata = NULL) + { + parent::__construct($message, $code, $previous); + $this->responsedata = $responsedata; + } + + public function getResponseData(): ?array + { + return $this->responsedata; + } +} diff --git a/src/Exceptions/LEFunctionsException.php b/src/Exceptions/LEFunctionsException.php new file mode 100644 index 0000000..0cd3f5d --- /dev/null +++ b/src/Exceptions/LEFunctionsException.php @@ -0,0 +1,58 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEFunctionsException extends LEException +{ + public const INVALIDARGUMENTEXCEPTION = 0x51; + public const GENERATEKEYPAIREXCEPTION = 0x52; + public const PHPVERSIONEXCEPTION = 0x53; + + public static function InvalidArgumentException(string $message) + { + return new static($message, self::INVALIDARGUMENTEXCEPTION); + } + + public static function GenerateKeypairException(string $message) + { + return new static($message, self::GENERATEKEYPAIREXCEPTION); + } + + public static function PHPVersionException() + { + return new static('PHP 7.1+ required for EC keys.', self::PHPVERSIONEXCEPTION); + } +} diff --git a/src/Exceptions/LEOrderException.php b/src/Exceptions/LEOrderException.php new file mode 100644 index 0000000..ba24feb --- /dev/null +++ b/src/Exceptions/LEOrderException.php @@ -0,0 +1,70 @@ + + * @copyright 2020 Youri van Weegberg + * @license https://opensource.org/licenses/mit-license.php MIT License + * @link https://github.com/yourivw/LEClient + * @since Class available since Release 1.2.0 + */ +class LEOrderException extends LEException +{ + public const INVALIDKEYTYPEEXCEPTION = 0x31; + public const INVALIDORDERSTATUSEXCEPTION = 0x32; + public const CREATEFAILEDEXCEPTION = 0x33; + public const INVALIDARGUMENTEXCEPTION = 0x34; + public const INVALIDCONFIGURATIONEXCEPTION = 0x35; + + public static function InvalidKeyTypeException(string $keyType) + { + return new static(sprintf('Key type \'%s\' not supported.', $keyType), self::INVALIDKEYTYPEEXCEPTION); + } + + public static function InvalidOrderStatusException() + { + return new static('Order status is invalid.', self::INVALIDORDERSTATUSEXCEPTION); + } + + public static function CreateFailedException(string $message) + { + return new static($message, self::CREATEFAILEDEXCEPTION); + } + + public static function InvalidArgumentException(string $message) + { + return new static($message, self::INVALIDARGUMENTEXCEPTION); + } + + public static function InvalidConfigurationException(string $message) + { + return new static($message, self::INVALIDCONFIGURATIONEXCEPTION); + } +} diff --git a/src/LEAccount.php b/src/LEAccount.php index 027c4e0..19e627f 100644 --- a/src/LEAccount.php +++ b/src/LEAccount.php @@ -2,6 +2,8 @@ namespace LEClient; +use LEClient\Exceptions\LEAccountException; + /** * LetsEncrypt Account class, containing the functions and data associated with a LetsEncrypt account. * @@ -79,7 +81,7 @@ public function __construct($connector, $log, $email, $accountKeys) { $this->connector->accountURL = $this->getLEAccount(); } - if($this->connector->accountURL == false) throw new \RuntimeException('Account not found or deactivated.'); + if($this->connector->accountURL == false) throw LEAccountException::AccountNotFoundException(); $this->getLEAccountData(); } @@ -139,7 +141,7 @@ private function getLEAccountData() } else { - throw new \RuntimeException('Account data cannot be found.'); + throw LEAccountException::AccountNotFoundException(); } } @@ -158,7 +160,7 @@ public function updateAccount($email) $post = $this->connector->post($this->connector->accountURL, $sign); if($post['status'] === 200) { - $this->id = $post['body']['id']; + $this->id = isset($post['body']['id']) ? $post['body']['id'] : ''; $this->key = $post['body']['key']; $this->contact = $post['body']['contact']; $this->agreement = isset($post['body']['agreement']) ? $post['body']['agreement'] : ''; @@ -235,6 +237,8 @@ public function deactivateAccount() $this->log->info('Account deactivated.'); } elseif($this->log >= LEClient::LOG_STATUS) LEFunctions::log('Account deactivated.', 'function deactivateAccount'); + + return true; } else { diff --git a/src/LEAuthorization.php b/src/LEAuthorization.php index 8277e7b..1d2f180 100644 --- a/src/LEAuthorization.php +++ b/src/LEAuthorization.php @@ -2,6 +2,8 @@ namespace LEClient; +use LEClient\Exceptions\LEAuthorizationException; + /** * LetsEncrypt Authorization class, getting LetsEncrypt authorization data associated with a LetsEncrypt Order instance. * @@ -60,13 +62,14 @@ public function __construct($connector, $log, $authorizationURL) $this->log = $log; $this->authorizationURL = $authorizationURL; - $get = $this->connector->get($this->authorizationURL); - if($get['status'] === 200) + $sign = $this->connector->signRequestKid('', $this->connector->accountURL, $this->authorizationURL); + $post = $this->connector->post($this->authorizationURL, $sign); + if($post['status'] === 200) { - $this->identifier = $get['body']['identifier']; - $this->status = $get['body']['status']; - $this->expires = $get['body']['expires']; - $this->challenges = $get['body']['challenges']; + $this->identifier = $post['body']['identifier']; + $this->status = $post['body']['status']; + $this->expires = $post['body']['expires']; + $this->challenges = $post['body']['challenges']; } else { @@ -84,13 +87,14 @@ public function __construct($connector, $log, $authorizationURL) public function updateData() { - $get = $this->connector->get($this->authorizationURL); - if($get['status'] === 200) + $sign = $this->connector->signRequestKid('', $this->connector->accountURL, $this->authorizationURL); + $post = $this->connector->post($this->authorizationURL, $sign); + if($post['status'] === 200) { - $this->identifier = $get['body']['identifier']; - $this->status = $get['body']['status']; - $this->expires = $get['body']['expires']; - $this->challenges = $get['body']['challenges']; + $this->identifier = $post['body']['identifier']; + $this->status = $post['body']['status']; + $this->expires = $post['body']['expires']; + $this->challenges = $post['body']['challenges']; } else { @@ -116,6 +120,6 @@ public function getChallenge($type) { if($challenge['type'] == $type) return $challenge; } - throw new \RuntimeException('No challenge found for type \'' . $type . '\' and identifier \'' . $this->identifier['value'] . '\'.'); + throw LEAuthorizationException::NoChallengeFoundException($type, $this->identifier['value']); } } diff --git a/src/LEClient.php b/src/LEClient.php index defe5bf..7db15c4 100644 --- a/src/LEClient.php +++ b/src/LEClient.php @@ -2,6 +2,8 @@ namespace LEClient; +use LEClient\Exceptions\LEClientException; + /** * Main LetsEncrypt Client class, works as a framework for the LEConnector, LEAccount, LEOrder and LEAuthorization classes. * @@ -65,7 +67,6 @@ class LEClient */ public function __construct($email, $acmeURL = LEClient::LE_PRODUCTION, $log = LEClient::LOG_OFF, $certificateKeys = 'keys/', $accountKeys = '__account/') { - $this->log = $log; if (is_bool($acmeURL)) @@ -77,10 +78,10 @@ public function __construct($email, $acmeURL = LEClient::LE_PRODUCTION, $log = L { $this->baseURL = $acmeURL; } - else throw new \RuntimeException('acmeURL must be set to string or bool (legacy).'); + else throw LEClientException::InvalidArgumentException('acmeURL must be set to string or bool (legacy).'); - if (is_array($certificateKeys) && is_string($accountKeys)) throw new \RuntimeException('When certificateKeys is array, accountKeys must be array too.'); - elseif (is_array($accountKeys) && is_string($certificateKeys)) throw new \RuntimeException('When accountKeys is array, certificateKeys must be array too.'); + if (is_array($certificateKeys) && is_string($accountKeys)) throw LEClientException::InvalidArgumentException('When certificateKeys is array, accountKeys must be array too.'); + elseif (is_array($accountKeys) && is_string($certificateKeys)) throw LEClientException::InvalidArgumentException('When accountKeys is array, certificateKeys must be array too.'); if (is_string($certificateKeys)) { @@ -102,21 +103,21 @@ public function __construct($email, $acmeURL = LEClient::LE_PRODUCTION, $log = L } elseif (is_array($certificateKeys)) { - if (!isset($certificateKeys['certificate']) && !isset($certificateKeys['fullchain_certificate'])) throw new \RuntimeException('certificateKeys[certificate] or certificateKeys[fullchain_certificate] file path must be set.'); - if (!isset($certificateKeys['private_key'])) throw new \RuntimeException('certificateKeys[private_key] file path must be set.'); + if (!isset($certificateKeys['certificate']) && !isset($certificateKeys['fullchain_certificate'])) throw LEClientException::InvalidArgumentException('certificateKeys[certificate] or certificateKeys[fullchain_certificate] file path must be set.'); + if (!isset($certificateKeys['private_key'])) throw LEClientException::InvalidArgumentException('certificateKeys[private_key] file path must be set.'); if (!isset($certificateKeys['order'])) $certificateKeys['order'] = dirname($certificateKeys['private_key']).'/order'; if (!isset($certificateKeys['public_key'])) $certificateKeys['public_key'] = dirname($certificateKeys['private_key']).'/public.pem'; foreach ($certificateKeys as $param => $file) { $parentDir = dirname($file); - if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found.'); + if (!is_dir($parentDir)) throw LEClientException::InvalidDirectoryException($parentDir); } $this->certificateKeys = $certificateKeys; } else { - throw new \RuntimeException('certificateKeys must be string or array.'); + throw LEClientException::InvalidArgumentException('certificateKeys must be string or array.'); } if (is_string($accountKeys)) @@ -136,22 +137,21 @@ public function __construct($email, $acmeURL = LEClient::LE_PRODUCTION, $log = L } elseif (is_array($accountKeys)) { - if (!isset($accountKeys['private_key'])) throw new \RuntimeException('accountKeys[private_key] file path must be set.'); - if (!isset($accountKeys['public_key'])) throw new \RuntimeException('accountKeys[public_key] file path must be set.'); + if (!isset($accountKeys['private_key'])) throw LEClientException::InvalidArgumentException('accountKeys[private_key] file path must be set.'); + if (!isset($accountKeys['public_key'])) throw LEClientException::InvalidArgumentException('accountKeys[public_key] file path must be set.'); foreach ($accountKeys as $param => $file) { $parentDir = dirname($file); - if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found.'); + if (!is_dir($parentDir)) throw LEClientException::InvalidDirectoryException($parentDir); } $this->accountKeys = $accountKeys; } else { - throw new \RuntimeException('accountKeys must be string or array'); + throw LEClientException::InvalidArgumentException('accountKeys must be string or array.'); } - $this->connector = new LEConnector($this->log, $this->baseURL, $this->accountKeys); $this->account = new LEAccount($this->connector, $this->log, $email, $this->accountKeys); diff --git a/src/LEConnector.php b/src/LEConnector.php index 019c50f..0809a1f 100644 --- a/src/LEConnector.php +++ b/src/LEConnector.php @@ -2,6 +2,8 @@ namespace LEClient; +use LEClient\Exceptions\LEConnectorException; + /** * LetsEncrypt Connector class, containing the functions necessary to sign with JSON Web Key and Key ID, and perform GET, POST and HEAD requests. * @@ -87,7 +89,7 @@ private function getLEDirectory() */ private function getNewNonce() { - if($this->head($this->newNonce)['status'] !== 200) throw new \RuntimeException('No new nonce.'); + if($this->head($this->newNonce)['status'] !== 200) throw LEConnectorException::NoNewNonceException(); } /** @@ -101,7 +103,7 @@ private function getNewNonce() */ private function request($method, $URL, $data = null) { - if($this->accountDeactivated) throw new \RuntimeException('The account was deactivated. No further requests can be made.'); + if($this->accountDeactivated) throw LEConnectorException::AccountDeactivatedException(); $headers = array('Accept: application/json', 'Content-Type: application/jose+json'); $requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL; @@ -123,13 +125,13 @@ private function request($method, $URL, $data = null) curl_setopt($handle, CURLOPT_NOBODY, true); break; default: - throw new \RuntimeException('HTTP request ' . $method . ' not supported.'); + throw LEConnectorException::MethodNotSupportedException($method); break; } $response = curl_exec($handle); if(curl_errno($handle)) { - throw new \RuntimeException('Curl: ' . curl_error($handle)); + throw LEConnectorException::CurlErrorException(curl_error($handle)); } $headerSize = curl_getinfo($handle, CURLINFO_HEADER_SIZE); @@ -149,13 +151,7 @@ private function request($method, $URL, $data = null) $this->log->debug($method . ' response received', $jsonresponse); } elseif($this->log >= LEClient::LOG_DEBUG) LEFunctions::log($jsonresponse); - - if((($method == 'POST' OR $method == 'GET') AND $statusCode !== 200 AND $statusCode !== 201) OR - ($method == 'HEAD' AND $statusCode !== 200)) - { - throw new \RuntimeException('Invalid response, header: ' . $header); - } - + if(preg_match('~Replay\-Nonce: (\S+)~i', $header, $matches)) { $this->nonce = trim($matches[1]); @@ -165,6 +161,12 @@ private function request($method, $URL, $data = null) if($method == 'POST') $this->getNewNonce(); // Not expecting a new nonce with GET and HEAD requests. } + if((($method == 'POST' OR $method == 'GET') AND $statusCode !== 200 AND $statusCode !== 201) OR + ($method == 'HEAD' AND $statusCode !== 200)) + { + throw LEConnectorException::InvalidResponseException($jsonresponse); + } + return $jsonresponse; } diff --git a/src/LEFunctions.php b/src/LEFunctions.php index e90417e..edd9425 100644 --- a/src/LEFunctions.php +++ b/src/LEFunctions.php @@ -2,7 +2,7 @@ namespace LEClient; -use Exception; +use LEClient\Exceptions\LEFunctionsException; /** * LetsEncrypt Functions class, supplying the LetsEncrypt Client with supportive functions. @@ -49,8 +49,7 @@ class LEFunctions */ public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pem', $publicKeyFile = 'public.pem', $keySize = 4096) { - - if ($keySize < 2048 || $keySize > 4096) throw new \RuntimeException("RSA key size must be between 2048 and 4096."); + if ($keySize < 2048 || $keySize > 4096) throw LEFunctionsException::InvalidArgumentException('RSA key size must be between 2048 and 4096.'); $res = openssl_pkey_new(array( "private_key_type" => OPENSSL_KEYTYPE_RSA, @@ -62,7 +61,7 @@ public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pe while($message = openssl_error_string()){ $error .= $message.PHP_EOL; } - throw new \RuntimeException($error); + throw LEFunctionsException::GenerateKeypairException($error); } if(!openssl_pkey_export($res, $privateKey)) { @@ -70,7 +69,7 @@ public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pe while($message = openssl_error_string()){ $error .= $message.PHP_EOL; } - throw new \RuntimeException($error); + throw LEFunctionsException::GenerateKeypairException($error); } $details = openssl_pkey_get_details($res); @@ -87,8 +86,6 @@ public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pe openssl_pkey_free($res); } - - /** * Generates a new EC prime256v1 keypair and saves both keys to a new file. * @@ -99,26 +96,26 @@ public static function RSAGenerateKeys($directory, $privateKeyFile = 'private.pe */ public static function ECGenerateKeys($directory, $privateKeyFile = 'private.pem', $publicKeyFile = 'public.pem', $keySize = 256) { - if (version_compare(PHP_VERSION, '7.1.0') == -1) throw new \RuntimeException("PHP 7.1+ required for EC keys."); + if (version_compare(PHP_VERSION, '7.1.0') == -1) throw LEFunctionsException::PHPVersionException(); if ($keySize == 256) { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_EC, - "curve_name" => "prime256v1", - )); + $res = openssl_pkey_new(array( + "private_key_type" => OPENSSL_KEYTYPE_EC, + "curve_name" => "prime256v1", + )); } elseif ($keySize == 384) { - $res = openssl_pkey_new(array( - "private_key_type" => OPENSSL_KEYTYPE_EC, - "curve_name" => "secp384r1", - )); + $res = openssl_pkey_new(array( + "private_key_type" => OPENSSL_KEYTYPE_EC, + "curve_name" => "secp384r1", + )); } - else throw new \RuntimeException("EC key size must be 256 or 384."); + else throw LEFunctionsException::InvalidArgumentException('EC key size must be 256 or 384.'); - if(!openssl_pkey_export($res, $privateKey)) throw new \RuntimeException("EC keypair export failed!"); + if(!openssl_pkey_export($res, $privateKey)) throw LEFunctionsException::GenerateKeypairException('EC keypair export failed!'); $details = openssl_pkey_get_details($res); diff --git a/src/LEOrder.php b/src/LEOrder.php index 159229c..63a64b6 100644 --- a/src/LEOrder.php +++ b/src/LEOrder.php @@ -2,6 +2,8 @@ namespace LEClient; +use LEClient\Exceptions\LEOrderException; + /** * LetsEncrypt Order class, containing the functions and data associated with a specific LetsEncrypt order. * @@ -96,7 +98,12 @@ public function __construct($connector, $log, $certificateKeys, $basename, $doma $this->keyType = $keyTypeParts[0][1]; $this->keySize = intval($keyTypeParts[0][2]); } - else throw new \RuntimeException('Key type \'' . $keyType . '\' not supported.'); + else throw LEOrderException::InvalidKeyTypeException($keyType); + } + + if(preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notBefore) == false OR preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notAfter) == false) + { + throw LEOrderException::InvalidArgumentException('notBefore and notAfter fields must be empty or be a string similar to 0000-00-00T00:00:00Z'); } $this->certificateKeys = $certificateKeys; @@ -104,17 +111,18 @@ public function __construct($connector, $log, $certificateKeys, $basename, $doma if(file_exists($this->certificateKeys['private_key']) AND file_exists($this->certificateKeys['order']) AND file_exists($this->certificateKeys['public_key'])) { $this->orderURL = file_get_contents($this->certificateKeys['order']); - if (filter_var($this->orderURL, FILTER_VALIDATE_URL)) + if (filter_var($this->orderURL, FILTER_VALIDATE_URL) !== false) { try { - $get = $this->connector->get($this->orderURL); - if($get['body']['status'] == "invalid") + $sign = $this->connector->signRequestKid('', $this->connector->accountURL, $this->orderURL); + $post = $this->connector->post($this->orderURL, $sign); + if($post['body']['status'] == "invalid") { - throw new \RuntimeException('Order status is invalid'); + throw LEOrderException::InvalidOrderStatusException(); } - $orderdomains = array_map(function($ident) { return $ident['value']; }, $get['body']['identifiers']); + $orderdomains = array_map(function($ident) { return $ident['value']; }, $post['body']['identifiers']); $diff = array_merge(array_diff($orderdomains, $domains), array_diff($domains, $orderdomains)); if(!empty($diff)) { @@ -131,12 +139,12 @@ public function __construct($connector, $log, $certificateKeys, $basename, $doma } else { - $this->status = $get['body']['status']; - $this->expires = $get['body']['expires']; - $this->identifiers = $get['body']['identifiers']; - $this->authorizationURLs = $get['body']['authorizations']; - $this->finalizeURL = $get['body']['finalize']; - if(array_key_exists('certificate', $get['body'])) $this->certificateURL = $get['body']['certificate']; + $this->status = $post['body']['status']; + $this->expires = $post['body']['expires']; + $this->identifiers = $post['body']['identifiers']; + $this->authorizationURLs = $post['body']['authorizations']; + $this->finalizeURL = $post['body']['finalize']; + if(array_key_exists('certificate', $post['body'])) $this->certificateURL = $post['body']['certificate']; $this->updateAuthorizations(); } } @@ -190,65 +198,57 @@ public function __construct($connector, $log, $certificateKeys, $basename, $doma */ private function createOrder($domains, $notBefore, $notAfter) { - if(preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notBefore) AND preg_match('~(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z|^$)~', $notAfter)) + $dns = array(); + foreach($domains as $domain) { + if(preg_match_all('~(\*\.)~', $domain) > 1) throw LEOrderException::InvalidArgumentException('Cannot create orders with multiple wildcards in one domain.'); + $dns[] = array('type' => 'dns', 'value' => $domain); + } + $payload = array("identifiers" => $dns, 'notBefore' => $notBefore, 'notAfter' => $notAfter); + $sign = $this->connector->signRequestKid($payload, $this->connector->accountURL, $this->connector->newOrder); + $post = $this->connector->post($this->connector->newOrder, $sign); - $dns = array(); - foreach($domains as $domain) - { - if(preg_match_all('~(\*\.)~', $domain) > 1) throw new \RuntimeException('Cannot create orders with multiple wildcards in one domain.'); - $dns[] = array('type' => 'dns', 'value' => $domain); - } - $payload = array("identifiers" => $dns, 'notBefore' => $notBefore, 'notAfter' => $notAfter); - $sign = $this->connector->signRequestKid($payload, $this->connector->accountURL, $this->connector->newOrder); - $post = $this->connector->post($this->connector->newOrder, $sign); - - if($post['status'] === 201) + if($post['status'] === 201) + { + if(preg_match('~Location: (\S+)~i', $post['header'], $matches)) { - if(preg_match('~Location: (\S+)~i', $post['header'], $matches)) + $this->orderURL = trim($matches[1]); + file_put_contents($this->certificateKeys['order'], $this->orderURL); + if ($this->keyType == "rsa") { - $this->orderURL = trim($matches[1]); - file_put_contents($this->certificateKeys['order'], $this->orderURL); - if ($this->keyType == "rsa") - { - LEFunctions::RSAgenerateKeys(null, $this->certificateKeys['private_key'], $this->certificateKeys['public_key'], $this->keySize); - } - elseif ($this->keyType == "ec") - { - LEFunctions::ECgenerateKeys(null, $this->certificateKeys['private_key'], $this->certificateKeys['public_key'], $this->keySize); - } - else - { - throw new \RuntimeException('Key type \'' . $this->keyType . '\' not supported.'); - } - - $this->status = $post['body']['status']; - $this->expires = $post['body']['expires']; - $this->identifiers = $post['body']['identifiers']; - $this->authorizationURLs = $post['body']['authorizations']; - $this->finalizeURL = $post['body']['finalize']; - if(array_key_exists('certificate', $post['body'])) $this->certificateURL = $post['body']['certificate']; - $this->updateAuthorizations(); - - if($this->log instanceof \Psr\Log\LoggerInterface) - { - $this->log->info('Created order for \'' . $this->basename . '\'.'); - } - elseif($this->log >= LEClient::LOG_STATUS) LEFunctions::log('Created order for \'' . $this->basename . '\'.', 'function createOrder (function LEOrder __construct)'); + LEFunctions::RSAgenerateKeys(null, $this->certificateKeys['private_key'], $this->certificateKeys['public_key'], $this->keySize); + } + elseif ($this->keyType == "ec") + { + LEFunctions::ECgenerateKeys(null, $this->certificateKeys['private_key'], $this->certificateKeys['public_key'], $this->keySize); } else { - throw new \RuntimeException('New-order returned invalid response.'); + throw LEOrderException::InvalidKeyTypeException($this->keyType); + } + + $this->status = $post['body']['status']; + $this->expires = $post['body']['expires']; + $this->identifiers = $post['body']['identifiers']; + $this->authorizationURLs = $post['body']['authorizations']; + $this->finalizeURL = $post['body']['finalize']; + if(array_key_exists('certificate', $post['body'])) $this->certificateURL = $post['body']['certificate']; + $this->updateAuthorizations(); + + if($this->log instanceof \Psr\Log\LoggerInterface) + { + $this->log->info('Created order for \'' . $this->basename . '\'.'); } + elseif($this->log >= LEClient::LOG_STATUS) LEFunctions::log('Created order for \'' . $this->basename . '\'.', 'function createOrder (function LEOrder __construct)'); } else { - throw new \RuntimeException('Creating new order failed.'); + throw LEOrderException::CreateFailedException('New-order returned invalid response.'); } } else { - throw new \RuntimeException('notBefore and notAfter fields must be empty or be a string similar to 0000-00-00T00:00:00Z'); + throw LEOrderException::CreateFailedException('Creating new order failed.'); } } @@ -257,15 +257,16 @@ private function createOrder($domains, $notBefore, $notAfter) */ private function updateOrderData() { - $get = $this->connector->get($this->orderURL); - if($get['status'] === 200) + $sign = $this->connector->signRequestKid('', $this->connector->accountURL, $this->orderURL); + $post = $this->connector->post($this->orderURL, $sign); + if($post['status'] === 200) { - $this->status = $get['body']['status']; - $this->expires = $get['body']['expires']; - $this->identifiers = $get['body']['identifiers']; - $this->authorizationURLs = $get['body']['authorizations']; - $this->finalizeURL = $get['body']['finalize']; - if(array_key_exists('certificate', $get['body'])) $this->certificateURL = $get['body']['certificate']; + $this->status = $post['body']['status']; + $this->expires = $post['body']['expires']; + $this->identifiers = $post['body']['identifiers']; + $this->authorizationURLs = $post['body']['authorizations']; + $this->finalizeURL = $post['body']['finalize']; + if(array_key_exists('certificate', $post['body'])) $this->certificateURL = $post['body']['certificate']; $this->updateAuthorizations(); } else @@ -647,10 +648,11 @@ public function getCertificate() } if($this->status == 'valid' && !empty($this->certificateURL)) { - $get = $this->connector->get($this->certificateURL); - if($get['status'] === 200) + $sign = $this->connector->signRequestKid('', $this->connector->accountURL, $this->certificateURL); + $post = $this->connector->post($this->certificateURL, $sign); + if($post['status'] === 200) { - if(preg_match_all('~(-----BEGIN\sCERTIFICATE-----[\s\S]+?-----END\sCERTIFICATE-----)~i', $get['body'], $matches)) + if(preg_match_all('~(-----BEGIN\sCERTIFICATE-----[\s\S]+?-----END\sCERTIFICATE-----)~i', $post['body'], $matches)) { if (isset($this->certificateKeys['certificate'])) file_put_contents($this->certificateKeys['certificate'], $matches[0][0]); @@ -713,7 +715,7 @@ public function revokeCertificate($reason = 0) { if (isset($this->certificateKeys['certificate'])) $certFile = $this->certificateKeys['certificate']; elseif (isset($this->certificateKeys['fullchain_certificate'])) $certFile = $this->certificateKeys['fullchain_certificate']; - else throw new \RuntimeException('certificateKeys[certificate] or certificateKeys[fullchain_certificate] required'); + else throw LEOrderException::InvalidConfigurationException('certificateKeys[certificate] or certificateKeys[fullchain_certificate] required'); if(file_exists($certFile) && file_exists($this->certificateKeys['private_key'])) {