diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..cd8eb86
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,15 @@
+; This file is for unifying the coding style for different editors and IDEs.
+; More information at http://editorconfig.org
+
+root = true
+
+[*]
+charset = utf-8
+indent_size = 4
+indent_style = space
+end_of_line = lf
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..3286141
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+# Path-based git attributes
+# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
+
+# Ignore all test and documentation with "export-ignore".
+/.gitattributes export-ignore
+/.gitignore export-ignore
+/.travis.yml export-ignore
+/phpunit.xml.dist export-ignore
+/.scrutinizer.yml export-ignore
+/tests export-ignore
+/docs export-ignore
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..81f3e08
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+.idea
+build
+composer.lock
+vendor
+examples/keys
+certificates
+keys
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
new file mode 100644
index 0000000..244d880
--- /dev/null
+++ b/.scrutinizer.yml
@@ -0,0 +1,23 @@
+filter:
+ excluded_paths: [tests/*]
+
+checks:
+ php:
+ remove_extra_empty_lines: true
+ remove_php_closing_tag: true
+ remove_trailing_whitespace: true
+ fix_use_statements:
+ remove_unused: true
+ preserve_multiple: false
+ preserve_blanklines: true
+ order_alphabetically: true
+ fix_php_opening_tag: true
+ fix_linefeed: true
+ fix_line_ending: true
+ fix_identation_4spaces: true
+ fix_doc_comments: true
+
+tools:
+ external_code_coverage:
+ timeout: 600
+ runs: 2
diff --git a/.styleci.yml b/.styleci.yml
new file mode 100644
index 0000000..247a09c
--- /dev/null
+++ b/.styleci.yml
@@ -0,0 +1 @@
+preset: psr2
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..da820c6
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,34 @@
+dist: trusty
+language: php
+
+php:
+ - 7.0
+ - 7.1
+
+# This triggers builds to run on the new TravisCI infrastructure.
+# See: http://docs.travis-ci.com/user/workers/container-based-infrastructure/
+sudo: false
+
+## Cache composer
+cache:
+ directories:
+ - $HOME/.composer/cache
+
+#matrix:
+# include:
+# - php: 5.6
+# env: 'COMPOSER_FLAGS="--prefer-stable --prefer-lowest"'
+
+before_script:
+ - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist
+
+script:
+ - vendor/bin/phpcs --standard=psr2 src/
+ - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover
+
+after_script:
+ - |
+ if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' && "$TRAVIS_PHP_VERSION" != '7.0' ]]; then
+ wget https://scrutinizer-ci.com/ocular.phar
+ php ocular.phar code-coverage:upload --format=php-clover coverage.clover
+ fi
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..5c7e541
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,12 @@
+# Changelog
+
+All notable changes to `php-certificate-toolbox` will be documented in this file.
+
+Updates should follow the [Keep a CHANGELOG](http://keepachangelog.com/) principles.
+
+## 0.1.0
+
+First release after major refactoring of [LEClient](https://github.com/yourivw/leclient)
+to be composer installable and testable, as well as support for alternative storage
+systems.
+
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..d994296
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,74 @@
+# Contributor Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as
+contributors and maintainers pledge to make participation in our project and
+our community a harassment-free experience for everyone, regardless of age, body
+size, disability, ethnicity, gender identity and expression, level of experience,
+nationality, personal appearance, race, religion, or sexual identity and
+orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment
+include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or
+advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic
+ address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a
+ professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable
+behavior and are expected to take appropriate and fair corrective action in
+response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or
+reject comments, commits, code, wiki edits, issues, and other contributions
+that are not aligned to this Code of Conduct, or to ban temporarily or
+permanently any contributor for other behaviors that they deem inappropriate,
+threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces
+when an individual is representing the project or its community. Examples of
+representing a project or community include using an official project e-mail
+address, posting via an official social media account, or acting as an appointed
+representative at an online or offline event. Representation of a project may be
+further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported by contacting the project team at `paul@elphin.com`. All
+complaints will be reviewed and investigated and will result in a response that
+is deemed necessary and appropriate to the circumstances. The project team is
+obligated to maintain confidentiality with regard to the reporter of an incident.
+Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good
+faith may face temporary or permanent repercussions as determined by other
+members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
+available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000..672abac
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,66 @@
+# Contributing
+
+Contributions are **welcome** and will be fully **credited**. This page details how to
+contribute and the expected code quality for all contributions.
+
+## Pull Requests
+
+We accept contributions via Pull Requests on [Github](https://github.com/lordelph/php-certificate-toolbox).
+
+- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``.
+
+- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
+
+- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
+
+- **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option.
+
+- **Create feature branches** - Don't ask us to pull from your master branch.
+
+- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
+
+- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](http://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
+
+
+## Running Tests
+
+``` bash
+$ composer test
+```
+
+## Exceptions
+
+* All exceptions thrown by code in this package MUST implement `LEClientException`
+* Custom exception classes SHOULD derive from standard base exceptions where appropriate
+* a `LogicException` SHOULD be used for invalid use of methods or classses which would be
+ fixable by the developer using the classes
+* a `RuntimeException` SHOULD be used for problems which arise from unexpected external
+ conditions, such as an ACME API failure.
+* It is not necessary to add code coverage for runtime exceptions - such code paths SHOULD
+ be marked with `@codeCoverageIgnoreStart` / `@codeCoverageIgnoreEnd` markers
+
+## Logging
+
+The classes use a PSR-3 compatible logger. The following should be used as a guideline
+for appropriate logging levels:
+
+* `debug` is for maintainer use only. If an end-user has an issue, they should be asked to
+ submit a report which contains a log with debug enabled. This should allow the interactions
+ with the remote ACME API to be observed.
+* `info` should record a general interaction which an outside observer would find interesting,
+ typically, that a high level method of the main client class has been used.
+* `notice` should record some expected change of state, e.g. a new order, new certificate etc
+* `warning` should record an unusual but handled problem, e.g. regenerating a private key
+* `error` should record an unusual but unhandled problem
+* `critical` should record any logic problem where the problem is likely correctable by the
+ code using these classes. It will usually be followed by a `LogicException`
+* `alert` should record unexpected issues arising from ACME API interactions, and will
+ generally be followed by a `RuntimeException`
+* `emergency` should be used only when time is of the essence. This is not presently used
+ but one example might be failure to renew a certificate when an existing certificate is
+ known to be expiring soon
+
+
+
+
+**Happy coding**!
diff --git a/ISSUE_TEMPLATE.md b/ISSUE_TEMPLATE.md
new file mode 100644
index 0000000..5b48c57
--- /dev/null
+++ b/ISSUE_TEMPLATE.md
@@ -0,0 +1,27 @@
+
+
+## Detailed description
+
+Provide a detailed description of the change or addition you are proposing.
+
+Make it clear if the issue is a bug, an enhancement or just a question.
+
+## Context
+
+Why is this change important to you? How would you use it?
+
+How can it benefit other users?
+
+## Possible implementation
+
+Not obligatory, but suggest an idea for implementing addition or change.
+
+## Your environment
+
+Include as many relevant details about the environment you experienced the bug in and how to reproduce it.
+
+* Version used (e.g. PHP 5.6, HHVM 3):
+* Operating system and version (e.g. Ubuntu 16.04, Windows 7):
+* Link to your project:
+* ...
+* ...
diff --git a/LEClient/LEClient.php b/LEClient/LEClient.php
deleted file mode 100644
index 3ea7e56..0000000
--- a/LEClient/LEClient.php
+++ /dev/null
@@ -1,200 +0,0 @@
-
- * @copyright 2018 Youri van Weegberg
- * @license https://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0
- * @link https://github.com/yourivw/LEClient
- * @since Class available since Release 1.0.0
- */
-class LEClient
-{
- const LE_PRODUCTION = 'https://acme-v02.api.letsencrypt.org';
- const LE_STAGING = 'https://acme-staging-v02.api.letsencrypt.org';
-
- private $certificatesKeys;
- private $accountKeys;
-
- private $connector;
- private $account;
-
- private $log;
-
- const LOG_OFF = 0; // Logs no messages or faults, except Runtime Exceptions.
- const LOG_STATUS = 1; // Logs only messages and faults.
- const LOG_DEBUG = 2; // Logs messages, faults and raw responses from HTTP requests.
-
- /**
- * Initiates the LetsEncrypt main client.
- *
- * @param array $email The array of strings containing e-mail addresses. Only used in this function when creating a new account.
- * @param boolean $acmeURL ACME URL, can be string or one of predefined values: LE_STAGING or LE_PRODUCTION. Defaults to LE_STAGING.
- * @param int $log The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted. Defaults to LOG_OFF. (optional)
- * @param string $certificateKeys The main directory in which all keys (and certificates), including account keys, are stored. Defaults to 'keys/'. (optional)
- * @param array $certificateKeys Optional array containing location of all certificate files. Required paths are public_key, private_key, order and certificate/fullchain_certificate (you can use both or only one of them)
- * @param string $accountKeys The directory in which the account keys are stored. Is a subdir inside $certificateKeys. Defaults to '__account/'.(optional)
- * @param array $accountKeys Optional array containing location of account private and public keys. Required paths are private_key, public_key.
- */
- public function __construct($email, $acmeURL = LEClient::LE_STAGING, $log = LEClient::LOG_OFF, $certificateKeys = 'keys/', $accountKeys = '__account/')
- {
-
- $this->log = $log;
-
- if (is_bool($acmeURL))
- {
- if ($acmeURL === true) $this->baseURL = LEClient::LE_STAGING;
- elseif ($acmeURL === false) $this->baseURL = LEClient::LE_PRODUCTION;
- }
- elseif (is_string($acmeURL))
- {
- $this->baseURL = $acmeURL;
- }
- else throw new \RuntimeException('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 also');
- elseif (is_array($accountKeys) && is_string($certificateKeys)) throw new \RuntimeException('when accountKeys is array, certificateKeys must be array also');
-
- if (is_string($certificateKeys))
- {
-
- $certificateKeysDir = $certificateKeys;
-
- if(!file_exists($certificateKeys))
- {
- mkdir($certificateKeys, 0777, true);
- LEFunctions::createhtaccess($certificateKeys);
- }
-
- $this->certificateKeys = array(
- "public_key" => $certificateKeys.'/public.pem',
- "private_key" => $certificateKeys.'/private.pem',
- "certificate" => $certificateKeys.'/certificate.crt',
- "fullchain_certificate" => $certificateKeys.'/fullchain.crt',
- "order" => $certificateKeys.'/order'
- );
-
- }
- 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['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');
- }
-
- $this->certificateKeys = $certificateKeys;
-
- }
- else
- {
- throw new \RuntimeException('certificateKeys must be string or array');
- }
-
- if (is_string($accountKeys))
- {
-
- $accountKeys = $certificateKeysDir.'/'.$accountKeys;
-
- if(!file_exists($accountKeys))
- {
- mkdir($accountKeys, 0777, true);
- LEFunctions::createhtaccess($accountKeys);
- }
-
- $this->accountKeys = array(
- "private_key" => $accountKeys.'/private.pem',
- "public_key" => $accountKeys.'/public.pem'
- );
- }
- 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');
-
- foreach ($accountKeys as $param => $file) {
- $parentDir = dirname($file);
- if (!is_dir($parentDir)) throw new \RuntimeException($parentDir.' directory not found');
- }
-
- $this->accountKeys = $accountKeys;
- }
- else
- {
- throw new \RuntimeException('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);
- if($this->log) LEFunctions::log('LEClient finished constructing', 'function LEClient __construct');
- }
-
-
- /**
- * Returns the LetsEncrypt account used in the current client.
- *
- * @return LEAccount The LetsEncrypt Account instance used by the client.
- */
- public function getAccount()
- {
- return $this->account;
- }
-
- /**
- * Returns a LetsEncrypt order. If an order exists, this one is returned. If not, a new order is created and returned.
- *
- * @param string $basename The base name for the order. Preferable the top domain (example.org). Will be the directory in which the keys are stored. Used for the CommonName in the certificate as well.
- * @param array $domains The array of strings containing the domain names on the certificate.
- * @param string $keyType Type of the key we want to use for certificate. Can be provided in ALGO-SIZE format (ex. rsa-4096 or ec-256) or simple "rsa" and "ec" (using default sizes)
- * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss) at which the certificate becomes valid. Defaults to the moment the order is finalized. (optional)
- * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss) until which the certificate is valid. Defaults to 90 days past the moment the order is finalized. (optional)
- *
- * @return LEOrder The LetsEncrypt Order instance which is either retrieved or created.
- */
- public function getOrCreateOrder($basename, $domains, $keyType = 'rsa-4096', $notBefore = '', $notAfter = '')
- {
- return new LEOrder($this->connector, $this->log, $this->certificateKeys, $basename, $domains, $keyType, $notBefore, $notAfter);
- }
-}
-?>
diff --git a/LEClient/src/LEAccount.php b/LEClient/src/LEAccount.php
deleted file mode 100644
index bf1fe01..0000000
--- a/LEClient/src/LEAccount.php
+++ /dev/null
@@ -1,228 +0,0 @@
-
- * @copyright 2018 Youri van Weegberg
- * @license https://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0
- * @link https://github.com/yourivw/LEClient
- * @since Class available since Release 1.0.0
- */
-class LEAccount
-{
- private $connector;
- private $accountKeys;
-
- public $id;
- public $key;
- public $contact;
- public $agreement;
- public $initialIp;
- public $createdAt;
- public $status;
-
- private $log;
-
- /**
- * Initiates the LetsEncrypt Account class.
- *
- * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests.
- * @param int $log The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
- * @param array $email The array of strings containing e-mail addresses. Only used when creating a new account.
- * @param array $accountKeys Array containing location of account keys files.
- */
- public function __construct($connector, $log, $email, $accountKeys)
- {
- $this->connector = $connector;
- $this->accountKeys = $accountKeys;
- $this->log = $log;
-
- if(!file_exists($this->accountKeys['private_key']) OR !file_exists($this->accountKeys['public_key']))
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('No account found, attempting to create account.', 'function LEAccount __construct');
- LEFunctions::RSAgenerateKeys(null, $this->accountKeys['private_key'], $this->accountKeys['public_key']);
- $this->connector->accountURL = $this->createLEAccount($email);
- }
- else
- {
- $this->connector->accountURL = $this->getLEAccount();
- }
- if($this->connector->accountURL == false) throw new \RuntimeException('Account not found or deactivated.');
- $this->getLEAccountData();
- }
-
- /**
- * Creates a new LetsEncrypt account.
- *
- * @param array $email The array of strings containing e-mail addresses.
- *
- * @return object Returns the new account URL when the account was successfully created, false if not.
- */
- private function createLEAccount($email)
- {
- $contact = array_map(function($addr) { return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr); }, $email);
-
- $sign = $this->connector->signRequestJWK(array('contact' => $contact, 'termsOfServiceAgreed' => true), $this->connector->newAccount);
- $post = $this->connector->post($this->connector->newAccount, $sign);
- if(strpos($post['header'], "201 Created") !== false)
- {
- if(preg_match('~Location: (\S+)~i', $post['header'], $matches)) return trim($matches[1]);
- }
- return false;
- }
-
- /**
- * Gets the LetsEncrypt account URL associated with the stored account keys.
- *
- * @return object Returns the account URL if it is found, or false when none is found.
- */
- private function getLEAccount()
- {
- $sign = $this->connector->signRequestJWK(array('onlyReturnExisting' => true), $this->connector->newAccount);
- $post = $this->connector->post($this->connector->newAccount, $sign);
-
- if(strpos($post['header'], "200 OK") !== false)
- {
- if(preg_match('~Location: (\S+)~i', $post['header'], $matches)) return trim($matches[1]);
- }
- return false;
- }
-
- /**
- * Gets the LetsEncrypt account data from the account URL.
- */
- private function getLEAccountData()
- {
- $sign = $this->connector->signRequestKid(array('' => ''), $this->connector->accountURL, $this->connector->accountURL);
- $post = $this->connector->post($this->connector->accountURL, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- $this->id = $post['body']['id'];
- $this->key = $post['body']['key'];
- $this->contact = $post['body']['contact'];
- $this->agreement = $post['body']['agreement'];
- $this->initialIp = $post['body']['initialIp'];
- $this->createdAt = $post['body']['createdAt'];
- $this->status = $post['body']['status'];
- }
- else
- {
- throw new \RuntimeException('Account data cannot be found.');
- }
- }
-
- /**
- * Updates account data. Now just supporting new contact information.
- *
- * @param array $email The array of strings containing e-mail adresses.
- *
- * @return boolean Returns true if the update is successful, false if not.
- */
- public function updateAccount($email)
- {
- $contact = array_map(function($addr) { return empty($addr) ? '' : (strpos($addr, 'mailto') === false ? 'mailto:' . $addr : $addr); }, $email);
-
- $sign = $this->connector->signRequestKid(array('contact' => $contact), $this->connector->accountURL, $this->connector->accountURL);
- $post = $this->connector->post($this->connector->accountURL, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- $this->id = $post['body']['id'];
- $this->key = $post['body']['key'];
- $this->contact = $post['body']['contact'];
- $this->agreement = $post['body']['agreement'];
- $this->initialIp = $post['body']['initialIp'];
- $this->createdAt = $post['body']['createdAt'];
- $this->status = $post['body']['status'];
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Account data updated.', 'function updateAccount');
- return true;
- }
- else
- {
- return false;
- }
- }
-
- /**
- * Creates new RSA account keys and updates the keys with LetsEncrypt.
- *
- * @return boolean Returns true if the update is successful, false if not.
- */
- public function changeAccountKeys()
- {
- LEFunctions::RSAgenerateKeys(null, $this->accountKeys['private_key'].'.new', $this->accountKeys['public_key'].'.new');
- $privateKey = openssl_pkey_get_private(file_get_contents($this->accountKeys['private_key'].'.new'));
- $details = openssl_pkey_get_details($privateKey);
- $innerPayload = array('account' => $this->connector->accountURL, 'newKey' => array(
- "kty" => "RSA",
- "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"]),
- "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"])
- ));
- $outerPayload = $this->connector->signRequestJWK($innerPayload, $this->connector->keyChange, $this->accountKeys['private_key'].'.new');
- $sign = $this->connector->signRequestKid($outerPayload, $this->connector->accountURL, $this->connector->keyChange);
- $post = $this->connector->post($this->connector->keyChange, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- $this->getLEAccountData();
-
- unlink($this->accountKeys['private_key']);
- unlink($this->accountKeys['public_key']);
- rename($this->accountKeys['private_key'].'.new', $this->accountKeys['private_key']);
- rename($this->accountKeys['public_key'].'.new', $this->accountKeys['public_key']);
-
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Account keys changed.', 'function changeAccountKey');
- return true;
- }
- else
- {
- return false;
- }
- }
-
- /**
- * Deactivates the LetsEncrypt account.
- *
- * @return boolean Returns true if the deactivation is successful, false if not.
- */
- public function deactivateAccount()
- {
- $sign = $this->connector->signRequestKid(array('status' => 'deactivated'), $this->connector->accountURL, $this->connector->accountURL);
- $post = $this->connector->post($this->connector->accountURL, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- $this->connector->accountDeactivated = true;
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Account deactivated.', 'function deactivateAccount');
- }
- else
- {
- return false;
- }
- }
-}
-
-?>
diff --git a/LEClient/src/LEAuthorization.php b/LEClient/src/LEAuthorization.php
deleted file mode 100644
index 7ea82a0..0000000
--- a/LEClient/src/LEAuthorization.php
+++ /dev/null
@@ -1,114 +0,0 @@
-
- * @copyright 2018 Youri van Weegberg
- * @license https://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0
- * @link https://github.com/yourivw/LEClient
- * @since Class available since Release 1.0.0
- */
-class LEAuthorization
-{
- private $connector;
-
- public $authorizationURL;
- public $identifier;
- public $status;
- public $expires;
- public $challenges;
-
- private $log;
-
- /**
- * Initiates the LetsEncrypt Authorization class. Child of a LetsEncrypt Order instance.
- *
- * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests.
- * @param int $log The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
- * @param string $authorizationURL The URL of the authorization, given by a LetsEncrypt order request.
- */
- public function __construct($connector, $log, $authorizationURL)
- {
- $this->connector = $connector;
- $this->log = $log;
- $this->authorizationURL = $authorizationURL;
-
- $get = $this->connector->get($this->authorizationURL);
- if(strpos($get['header'], "200 OK") !== false)
- {
- $this->identifier = $get['body']['identifier'];
- $this->status = $get['body']['status'];
- $this->expires = $get['body']['expires'];
- $this->challenges = $get['body']['challenges'];
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Cannot find authorization \'' . $authorizationURL . '\'.', 'function LEAuthorization __construct');
- }
- }
-
- /**
- * Updates the data associated with the current LetsEncrypt Authorization instance.
- */
-
- public function updateData()
- {
- $get = $this->connector->get($this->authorizationURL);
- if(strpos($get['header'], "200 OK") !== false)
- {
- $this->identifier = $get['body']['identifier'];
- $this->status = $get['body']['status'];
- $this->expires = $get['body']['expires'];
- $this->challenges = $get['body']['challenges'];
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Cannot find authorization \'' . $authorizationURL . '\'.', 'function updateData');
- }
- }
-
- /**
- * Gets the challenge of the given $type for this LetsEncrypt Authorization instance. Throws a Runtime Exception if the given $type is not found in this
- * LetsEncrypt Authorization instance.
- *
- * @param int $type The type of verification. Supporting LEOrder::CHALLENGE_TYPE_HTTP and LEOrder::CHALLENGE_TYPE_DNS.
- *
- * @return array Returns an array with the challenge of the requested $type.
- */
- public function getChallenge($type)
- {
- foreach($this->challenges as $challenge)
- {
- if($challenge['type'] == $type) return $challenge;
- }
- throw new \RuntimeException('No challenge found for type \'' . $type . '\' and identifier \'' . $this->identifier['value'] . '\'.');
- }
-}
-
-?>
\ No newline at end of file
diff --git a/LEClient/src/LEConnector.php b/LEClient/src/LEConnector.php
deleted file mode 100644
index 1bdc5fd..0000000
--- a/LEClient/src/LEConnector.php
+++ /dev/null
@@ -1,277 +0,0 @@
-
- * @copyright 2018 Youri van Weegberg
- * @license https://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0
- * @link https://github.com/yourivw/LEClient
- * @since Class available since Release 1.0.0
- */
-class LEConnector
-{
- public $baseURL;
- public $accountKeys;
-
- private $nonce;
-
- public $keyChange;
- public $newAccount;
- public $newNonce;
- public $newOrder;
- public $revokeCert;
-
- public $accountURL;
- public $accountDeactivated = false;
-
- private $log;
-
- /**
- * Initiates the LetsEncrypt Connector class.
- *
- * @param int $log The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
- * @param string $baseURL The LetsEncrypt server URL to make requests to.
- * @param array $accountKeys Array containing location of account keys files.
- */
- public function __construct($log, $baseURL, $accountKeys)
- {
- $this->baseURL = $baseURL;
- $this->accountKeys = $accountKeys;
- $this->log = $log;
- $this->getLEDirectory();
- $this->getNewNonce();
- }
-
- /**
- * Requests the LetsEncrypt Directory and stores the necessary URLs in this LetsEncrypt Connector instance.
- */
- private function getLEDirectory()
- {
- $req = $this->get('/directory');
- $this->keyChange = $req['body']['keyChange'];
- $this->newAccount = $req['body']['newAccount'];
- $this->newNonce = $req['body']['newNonce'];
- $this->newOrder = $req['body']['newOrder'];
- $this->revokeCert = $req['body']['revokeCert'];
- }
-
- /**
- * Requests a new nonce from the LetsEncrypt server and stores it in this LetsEncrypt Connector instance.
- */
- private function getNewNonce()
- {
- if(strpos($this->head($this->newNonce)['header'], "204 No Content") == false) throw new \RuntimeException('No new nonce.');
- }
-
- /**
- * Makes a Curl request.
- *
- * @param string $method The HTTP method to use. Accepting GET, POST and HEAD requests.
- * @param string $URL The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
- * @param object $data The body to attach to a POST request. Expected as a JSON encoded string.
- *
- * @return array Returns an array with the keys 'request', 'header' and 'body'.
- */
- private function request($method, $URL, $data = null)
- {
- if($this->accountDeactivated) throw new \RuntimeException('The account was deactivated. No further requests can be made.');
-
- $headers = array('Accept: application/json', 'Content-Type: application/json');
- $requestURL = preg_match('~^http~', $URL) ? $URL : $this->baseURL . $URL;
- $handle = curl_init();
- curl_setopt($handle, CURLOPT_URL, $requestURL);
- curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
- curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($handle, CURLOPT_HEADER, true);
-
- switch ($method) {
- case 'GET':
- break;
- case 'POST':
- curl_setopt($handle, CURLOPT_POST, true);
- curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
- break;
- case 'HEAD':
- curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'HEAD');
- curl_setopt($handle, CURLOPT_NOBODY, true);
- break;
- default:
- throw new \RuntimeException('HTTP request ' . $method . ' not supported.');
- 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);
- $jsonbody = json_decode($body, true);
- $jsonresponse = array('request' => $method . ' ' . $requestURL, 'header' => $header, 'body' => $jsonbody === null ? $body : $jsonbody);
- if($this->log >= LECLient::LOG_DEBUG) LEFunctions::log($jsonresponse);
-
- if( (($method == 'POST' OR $method == 'GET') AND strpos($header, "200 OK") === false AND strpos($header, "201 Created") === false) OR
- ($method == 'HEAD' AND strpos($header, "204 No Content") === false))
- {
- throw new \RuntimeException('Invalid response, header: ' . $header);
- }
-
- if(preg_match('~Replay\-Nonce: (\S+)~i', $header, $matches))
- {
- $this->nonce = trim($matches[1]);
- }
- else
- {
- if($method == 'POST') $this->getNewNonce(); // Not expecting a new nonce with GET and HEAD requests.
- }
-
- return $jsonresponse;
- }
-
- /**
- * Makes a GET request.
- *
- * @param string $url The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
- *
- * @return array Returns an array with the keys 'request', 'header' and 'body'.
- */
- public function get($url)
- {
- return $this->request('GET', $url);
- }
-
- /**
- * Makes a POST request.
- *
- * @param string $url The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
- * @param object $data The body to attach to a POST request. Expected as a json string.
- *
- * @return array Returns an array with the keys 'request', 'header' and 'body'.
- */
- public function post($url, $data = null)
- {
- return $this->request('POST', $url, $data);
- }
-
- /**
- * Makes a HEAD request.
- *
- * @param string $url The URL or partial URL to make the request to. If it is partial, the baseURL will be prepended.
- *
- * @return array Returns an array with the keys 'request', 'header' and 'body'.
- */
- public function head($url)
- {
- return $this->request('HEAD', $url);
- }
-
- /**
- * Generates a JSON Web Key signature to attach to the request.
- *
- * @param array $payload The payload to add to the signature.
- * @param string $url The URL to use in the signature.
- * @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
- *
- * @return string Returns a JSON encoded string containing the signature.
- */
- public function signRequestJWK($payload, $url, $privateKeyFile = '')
- {
- if($privateKeyFile == '') $privateKeyFile = $this->accountKeys['private_key'];
- $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile));
- $details = openssl_pkey_get_details($privateKey);
-
- $protected = array(
- "alg" => "RS256",
- "jwk" => array(
- "kty" => "RSA",
- "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"]),
- "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"]),
- ),
- "nonce" => $this->nonce,
- "url" => $url
- );
-
- $payload64 = LEFunctions::Base64UrlSafeEncode(str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload));
- $protected64 = LEFunctions::Base64UrlSafeEncode(json_encode($protected));
-
- openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
- $signed64 = LEFunctions::Base64UrlSafeEncode($signed);
-
- $data = array(
- 'protected' => $protected64,
- 'payload' => $payload64,
- 'signature' => $signed64
- );
-
- return json_encode($data);
- }
-
- /**
- * Generates a Key ID signature to attach to the request.
- *
- * @param array $payload The payload to add to the signature.
- * @param string $kid The Key ID to use in the signature.
- * @param string $url The URL to use in the signature.
- * @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. Defaults to accountKeys[private_key].
- *
- * @return string Returns a JSON encoded string containing the signature.
- */
- public function signRequestKid($payload, $kid, $url, $privateKeyFile = '')
- {
- if($privateKeyFile == '') $privateKeyFile = $this->accountKeys['private_key'];
- $privateKey = openssl_pkey_get_private(file_get_contents($privateKeyFile));
- $details = openssl_pkey_get_details($privateKey);
-
- $protected = array(
- "alg" => "RS256",
- "kid" => $kid,
- "nonce" => $this->nonce,
- "url" => $url
- );
-
- $payload64 = LEFunctions::Base64UrlSafeEncode(str_replace('\\/', '/', is_array($payload) ? json_encode($payload) : $payload));
- $protected64 = LEFunctions::Base64UrlSafeEncode(json_encode($protected));
-
- openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
- $signed64 = LEFunctions::Base64UrlSafeEncode($signed);
-
- $data = array(
- 'protected' => $protected64,
- 'payload' => $payload64,
- 'signature' => $signed64
- );
-
- return json_encode($data);
- }
-}
-
-?>
diff --git a/LEClient/src/LEFunctions.php b/LEClient/src/LEFunctions.php
deleted file mode 100644
index 7e59dda..0000000
--- a/LEClient/src/LEFunctions.php
+++ /dev/null
@@ -1,233 +0,0 @@
-
- * @copyright 2018 Youri van Weegberg
- * @license https://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0
- * @link https://github.com/yourivw/LEClient
- * @since Class available since Release 1.0.0
- */
-class LEFunctions
-{
- /**
- * Generates a new RSA keypair and saves both keys to a new file.
- *
- * @param string $directory The directory in which to store the new keys. If set to null or empty string - privateKeyFile and publicKeyFile will be treated as absolute paths.
- * @param string $privateKeyFile The filename for the private key file.
- * @param string $publicKeyFile The filename for the public key file.
- * @param string $keySize RSA key size, must be between 2048 and 4096 (default is 4096)
- */
- 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");
-
- $res = openssl_pkey_new(array(
- "private_key_type" => OPENSSL_KEYTYPE_RSA,
- "private_key_bits" => intval($keySize),
- ));
-
- if(!openssl_pkey_export($res, $privateKey)) throw new \RuntimeException("RSA keypair export failed!");
-
- $details = openssl_pkey_get_details($res);
-
- if ($directory !== null && $directory !== '')
- {
- $privateKeyFile = $directory.$privateKeyFile;
- $publicKeyFile = $directory.$publicKeyFile;
- }
-
- file_put_contents($privateKeyFile, $privateKey);
- file_put_contents($publicKeyFile, $details['key']);
-
- openssl_pkey_free($res);
- }
-
-
-
- /**
- * Generates a new EC prime256v1 keypair and saves both keys to a new file.
- *
- * @param string $directory The directory in which to store the new keys. If set to null or empty string - privateKeyFile and publicKeyFile will be treated as absolute paths.
- * @param string $privateKeyFile The filename for the private key file.
- * @param string $publicKeyFile The filename for the public key file.
- * @param string $keysize EC key size, possible values are 256 (prime256v1) or 384 (secp384r1), default is 256
- */
- 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 ($keySize == 256)
- {
- $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",
- ));
- }
- else throw new \RuntimeException("EC key size must be 256 or 384");
-
-
- if(!openssl_pkey_export($res, $privateKey)) throw new \RuntimeException("EC keypair export failed!");
-
- $details = openssl_pkey_get_details($res);
-
- if ($directory !== null && $directory !== '')
- {
- $privateKeyFile = $directory.$privateKeyFile;
- $publicKeyFile = $directory.$publicKeyFile;
- }
-
- file_put_contents($privateKeyFile, $privateKey);
- file_put_contents($publicKeyFile, $details['key']);
-
- openssl_pkey_free($res);
- }
-
-
-
- /**
- * Encodes a string input to a base64 encoded string which is URL safe.
- *
- * @param string $input The input string to encode.
- *
- * @return string Returns a URL safe base64 encoded string.
- */
- public static function Base64UrlSafeEncode($input)
- {
- return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
- }
-
- /**
- * Decodes a string that is URL safe base64 encoded.
- *
- * @param string $input The encoded input string to decode.
- *
- * @return string Returns the decoded input string.
- */
- public static function Base64UrlSafeDecode($input)
- {
- $remainder = strlen($input) % 4;
- if ($remainder) {
- $padlen = 4 - $remainder;
- $input .= str_repeat('=', $padlen);
- }
- return base64_decode(strtr($input, '-_', '+/'));
- }
-
-
-
- /**
- * Outputs a log message.
- *
- * @param object $data The data to print.
- * @param string $function The function name to print above. Defaults to the calling function's name from the stacktrace. (optional)
- */
- public static function log($data, $function = '')
- {
- $e = new Exception();
- $trace = $e->getTrace();
- $function = $function == '' ? 'function ' . $trace[3]['function'] . ' (function ' . $trace[2]['function'] . ')' : $function;
- if (PHP_SAPI == "cli")
- {
- echo '[' . date('d-m-Y H:i:s') . '] ' . $function . ":\n";
- print_r($data);
- echo "\n\n";
- }
- else
- {
- echo '' . date('d-m-Y H:i:s') . ', ' . $function . ': ';
- print_r($data);
- echo '
';
- }
- }
-
-
-
- /**
- * Makes a request to the HTTP challenge URL and checks whether the authorization is valid for the given $domain.
- *
- * @param string $domain The domain to check the authorization for.
- * @param string $token The token (filename) to request.
- * @param string $keyAuthorization the keyAuthorization (file content) to compare.
- *
- * @return boolean Returns true if the challenge is valid, false if not.
- */
- public static function checkHTTPChallenge($domain, $token, $keyAuthorization)
- {
- $requestURL = $domain . '/.well-known/acme-challenge/' . $token;
- $handle = curl_init();
- curl_setopt($handle, CURLOPT_URL, $requestURL);
- curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
- curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true);
- $response = curl_exec($handle);
- return (!empty($response) && $response == $keyAuthorization);
- }
-
- /**
- * Checks whether the applicable DNS TXT record is a valid authorization for the given $domain.
- *
- * @param string $domain The domain to check the authorization for.
- * @param string $DNSDigest The digest to compare the DNS record to.
- *
- * @return boolean Returns true if the challenge is valid, false if not.
- */
- public static function checkDNSChallenge($domain, $DNSDigest)
- {
- $DNS = '_acme-challenge.' . str_replace('*.', '', $domain);
- $records = dns_get_record($DNS, DNS_TXT);
- foreach($records as $record)
- {
- if($record['host'] == $DNS && $record['type'] == 'TXT' && $record['txt'] == $DNSDigest) return true;
- }
- return false;
- }
-
-
-
- /**
- * Creates a simple .htaccess file in $directory which denies from all.
- *
- * @param string $directory The directory in which to put the .htaccess file.
- */
- public static function createhtaccess($directory)
- {
- file_put_contents($directory . '.htaccess', "order deny,allow\ndeny from all");
- }
-}
-
-?>
diff --git a/LEClient/src/LEOrder.php b/LEClient/src/LEOrder.php
deleted file mode 100644
index bc235f0..0000000
--- a/LEClient/src/LEOrder.php
+++ /dev/null
@@ -1,657 +0,0 @@
-
- * @copyright 2018 Youri van Weegberg
- * @license https://opensource.org/licenses/mit-license.php MIT License
- * @version 1.1.0
- * @link https://github.com/yourivw/LEClient
- * @since Class available since Release 1.0.0
- */
-class LEOrder
-{
- private $connector;
-
- private $basename;
- private $certificateKeys;
- private $orderURL;
- private $keyType;
- private $keySize;
-
- public $status;
- public $expires;
- public $identifiers;
- private $authorizationURLs;
- public $authorizations;
- public $finalizeURL;
- public $certificateURL;
-
- private $log;
-
-
- const CHALLENGE_TYPE_HTTP = 'http-01';
- const CHALLENGE_TYPE_DNS = 'dns-01';
-
- /**
- * Initiates the LetsEncrypt Order class. If the base name is found in the $keysDir directory, the order data is requested. If no order was found locally, if the request is invalid or when there is a change in domain names, a new order is created.
- *
- * @param LEConnector $connector The LetsEncrypt Connector instance to use for HTTP requests.
- * @param int $log The level of logging. Defaults to no logging. LOG_OFF, LOG_STATUS, LOG_DEBUG accepted.
- * @param array $certificateKeys Array containing location of certificate keys files.
- * @param string $basename The base name for the order. Preferable the top domain (example.org). Will be the directory in which the keys are stored. Used for the CommonName in the certificate as well.
- * @param array $domains The array of strings containing the domain names on the certificate.
- * @param string $keyType Type of the key we want to use for certificate. Can be provided in ALGO-SIZE format (ex. rsa-4096 or ec-256) or simple "rsa" and "ec" (using default sizes)
- * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss) at which the certificate becomes valid.
- * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss) until which the certificate is valid.
- */
- public function __construct($connector, $log, $certificateKeys, $basename, $domains, $keyType = 'rsa-4096', $notBefore, $notAfter)
- {
- $this->connector = $connector;
- $this->basename = $basename;
- $this->log = $log;
-
- if ($keyType == 'rsa')
- {
- $this->keyType = 'rsa';
- $this->keySize = 4096;
- }
- elseif ($keyType == 'ec')
- {
- $this->keyType = 'ec';
- $this->keySize = 256;
- }
- else
- {
- preg_match_all('/^(rsa|ec)\-([0-9]{3,4})$/', $keyType, $keyTypeParts, PREG_SET_ORDER, 0);
-
- if (!empty($keyTypeParts))
- {
- $this->keyType = $keyTypeParts[0][1];
- $this->keySize = intval($keyTypeParts[0][2]);
- }
- else throw new \RuntimeException('Key type \'' . $keyType . '\' not supported.');
- }
-
- $this->certificateKeys = $certificateKeys;
-
- 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))
- {
- $get = $this->connector->get($this->orderURL);
- if(strpos($get['header'], "200 OK") !== false)
- {
- $orderdomains = array_map(function($ident) { return $ident['value']; }, $get['body']['identifiers']);
- $diff = array_merge(array_diff($orderdomains, $domains), array_diff($domains, $orderdomains));
- if(!empty($diff))
- {
- foreach ($this->certificateKeys as $file)
- {
- if (is_file($file)) rename($file, $file.'.old');
- }
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Domains do not match order data. Renaming current files and creating new order.', 'function LEOrder __construct');
- $this->createOrder($domains, $notBefore, $notAfter, $keyType);
- }
- 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->updateAuthorizations();
- }
- }
- else
- {
- foreach ($this->certificateKeys as $file)
- {
- if (is_file($file)) unlink($file);
- }
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Order data for \'' . $this->basename . '\' invalid. Deleting order data and creating new order.', 'function LEOrder __construct');
- $this->createOrder($domains, $notBefore, $notAfter);
- }
- }
- else
- {
-
- foreach ($this->certificateKeys as $file)
- {
- if (is_file($file)) unlink($file);
- }
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Order data for \'' . $this->basename . '\' invalid. Deleting order data and creating new order.', 'function LEOrder __construct');
-
- $this->createOrder($domains, $notBefore, $notAfter);
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('No order found for \'' . $this->basename . '\'. Creating new order.', 'function LEOrder __construct');
- $this->createOrder($domains, $notBefore, $notAfter);
- }
- }
-
- /**
- * Creates a new LetsEncrypt order and fills this instance with its data. Subsequently creates a new RSA keypair for the certificate.
- *
- * @param array $domains The array of strings containing the domain names on the certificate.
- * @param string $notBefore A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss) at which the certificate becomes valid.
- * @param string $notAfter A date string formatted like 0000-00-00T00:00:00Z (yyyy-mm-dd hh:mm:ss) until which the certificate is valid.
- */
- 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 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(strpos($post['header'], "201 Created") !== false)
- {
- 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")
- {
- 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 >= LECLient::LOG_STATUS) LEFunctions::log('Created order for \'' . $this->basename . '\'.', 'function createOrder (function LEOrder __construct)');
- }
- else
- {
- throw new \RuntimeException('New-order returned invalid response.');
- }
- }
- else
- {
- throw new \RuntimeException('Creating new order failed.');
- }
- }
- else
- {
- throw new \RuntimeException('notBefore and notAfter fields must be empty or be a string similar to 0000-00-00T00:00:00Z');
- }
- }
-
- /**
- * Fetches the latest data concerning this LetsEncrypt Order instance and fills this instance with the new data.
- */
- private function updateOrderData()
- {
- $get = $this->connector->get($this->orderURL);
- if(strpos($get['header'], "200 OK") !== false)
- {
- $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->updateAuthorizations();
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Cannot update data for order \'' . $this->basename . '\'.', 'function updateOrderData');
- }
- }
-
- /**
- * Fetches the latest data concerning all authorizations connected to this LetsEncrypt Order instance and creates and stores a new LetsEncrypt Authorization instance for each one.
- */
- private function updateAuthorizations()
- {
- $this->authorizations = array();
- foreach($this->authorizationURLs as $authURL)
- {
- if (filter_var($authURL, FILTER_VALIDATE_URL))
- {
- $auth = new LEAuthorization($this->connector, $this->log, $authURL);
- if($auth != false) $this->authorizations[] = $auth;
- }
- }
- }
-
- /**
- * Walks all LetsEncrypt Authorization instances and returns whether they are all valid (verified).
- *
- * @return boolean Returns true if all authorizations are valid (verified), returns false if not.
- */
- public function allAuthorizationsValid()
- {
- if(count($this->authorizations) > 0)
- {
- foreach($this->authorizations as $auth)
- {
- if($auth->status != 'valid') return false;
- }
- return true;
- }
- return false;
- }
-
- /**
- * Get all pending LetsEncrypt Authorization instances and return the necessary data for verification. The data in the return object depends on the $type.
- *
- * @param int $type The type of verification to get. Supporting http-01 and dns-01. Supporting LEOrder::CHALLENGE_TYPE_HTTP and LEOrder::CHALLENGE_TYPE_DNS. Throws
- * a Runtime Exception when requesting an unknown $type. Keep in mind a wildcard domain authorization only accepts LEOrder::CHALLENGE_TYPE_DNS.
- *
- * @return object Returns an array with verification data if successful, false if not pending LetsEncrypt Authorization instances were found. The return array always
- * contains 'type' and 'identifier'. For LEOrder::CHALLENGE_TYPE_HTTP, the array contains 'filename' and 'content' for necessary the authorization file.
- * For LEOrder::CHALLENGE_TYPE_DNS, the array contains 'DNSDigest', which is the content for the necessary DNS TXT entry.
- */
-
- public function getPendingAuthorizations($type)
- {
- $authorizations = array();
-
- $privateKey = openssl_pkey_get_private(file_get_contents($this->connector->accountKeys['private_key']));
- $details = openssl_pkey_get_details($privateKey);
-
- $header = array(
- "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"]),
- "kty" => "RSA",
- "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"])
-
- );
- $digest = LEFunctions::Base64UrlSafeEncode(hash('sha256', json_encode($header), true));
-
- foreach($this->authorizations as $auth)
- {
- if($auth->status == 'pending')
- {
- $challenge = $auth->getChallenge($type);
- if($challenge['status'] == 'pending')
- {
- $keyAuthorization = $challenge['token'] . '.' . $digest;
- switch(strtolower($type))
- {
- case LEOrder::CHALLENGE_TYPE_HTTP:
- $authorizations[] = array('type' => LEOrder::CHALLENGE_TYPE_HTTP, 'identifier' => $auth->identifier['value'], 'filename' => $challenge['token'], 'content' => $keyAuthorization);
- break;
- case LEOrder::CHALLENGE_TYPE_DNS:
- $DNSDigest = LEFunctions::Base64UrlSafeEncode(hash('sha256', $keyAuthorization, true));
- $authorizations[] = array('type' => LEOrder::CHALLENGE_TYPE_DNS, 'identifier' => $auth->identifier['value'], 'DNSDigest' => $DNSDigest);
- break;
- }
- }
- }
- }
-
- return count($authorizations) > 0 ? $authorizations : false;
- }
-
- /**
- * Sends a verification request for a given $identifier and $type. The function itself checks whether the verification is valid before making the request.
- * Updates the LetsEncrypt Authorization instances after a successful verification.
- *
- * @param string $identifier The domain name to verify.
- * @param int $type The type of verification. Supporting LEOrder::CHALLENGE_TYPE_HTTP and LEOrder::CHALLENGE_TYPE_DNS.
- *
- * @return boolean Returns true when the verification request was successful, false if not.
- */
- public function verifyPendingOrderAuthorization($identifier, $type)
- {
- $privateKey = openssl_pkey_get_private(file_get_contents($this->connector->accountKeys['private_key']));
- $details = openssl_pkey_get_details($privateKey);
-
- $header = array(
- "e" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["e"]),
- "kty" => "RSA",
- "n" => LEFunctions::Base64UrlSafeEncode($details["rsa"]["n"])
-
- );
- $digest = LEFunctions::Base64UrlSafeEncode(hash('sha256', json_encode($header), true));
-
- foreach($this->authorizations as $auth)
- {
- if($auth->identifier['value'] == $identifier)
- {
- if($auth->status == 'pending')
- {
- $challenge = $auth->getChallenge($type);
- if($challenge['status'] == 'pending')
- {
- $keyAuthorization = $challenge['token'] . '.' . $digest;
- switch($type)
- {
- case LEOrder::CHALLENGE_TYPE_HTTP:
- if(LEFunctions::checkHTTPChallenge($identifier, $challenge['token'], $keyAuthorization))
- {
- $sign = $this->connector->signRequestKid(array('keyAuthorization' => $keyAuthorization), $this->connector->accountURL, $challenge['url']);
- $post = $this->connector->post($challenge['url'], $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('HTTP challenge for \'' . $identifier . '\' valid.', 'function verifyPendingOrderAuthorization');
- while($auth->status == 'pending')
- {
- sleep(1);
- $auth->updateData();
- }
- return true;
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('HTTP challenge for \'' . $identifier . '\' tested, found invalid.', 'function verifyPendingOrderAuthorization');
- }
- break;
- case LEOrder::CHALLENGE_TYPE_DNS:
- $DNSDigest = LEFunctions::Base64UrlSafeEncode(hash('sha256', $keyAuthorization, true));
- if(LEFunctions::checkDNSChallenge($identifier, $DNSDigest))
- {
- $sign = $this->connector->signRequestKid(array('keyAuthorization' => $keyAuthorization), $this->connector->accountURL, $challenge['url']);
- $post = $this->connector->post($challenge['url'], $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('DNS challenge for \'' . $identifier . '\' valid.', 'function verifyPendingOrderAuthorization');
- while($auth->status == 'pending')
- {
- sleep(1);
- $auth->updateData();
- }
- return true;
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('DNS challenge for \'' . $identifier . '\' tested, found invalid.', 'function verifyPendingOrderAuthorization');
- }
- break;
- }
- }
- }
- }
- }
- return false;
- }
-
- /**
- * Deactivate an LetsEncrypt Authorization instance.
- *
- * @param string $identifier The domain name for which the verification should be deactivated.
- *
- * @return boolean Returns true is the deactivation request was successful, false if not.
- */
- public function deactivateOrderAuthorization($identifier)
- {
- foreach($this->authorizations as $auth)
- {
- if($auth->identifier['value'] == $identifier)
- {
- $sign = $this->connector->signRequestKid(array('status' => 'deactivated'), $this->connector->accountURL, $auth->authorizationURL);
- $post = $this->connector->post($auth->authorizationURL, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Authorization for \'' . $identifier . '\' deactivated.', 'function deactivateOrderAuthorization');
- $this->updateAuthorizations();
- return true;
- }
- }
- }
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('No authorization found for \'' . $identifier . '\', cannot deactivate.', 'function deactivateOrderAuthorization');
- return false;
- }
-
- /**
- * Generates a Certificate Signing Request for the identifiers in the current LetsEncrypt Order instance. If possible, the base name will be the certificate
- * common name and all domain names in this LetsEncrypt Order instance will be added to the Subject Alternative Names entry.
- *
- * @return string Returns the generated CSR as string, unprepared for LetsEncrypt. Preparation for the request happens in finalizeOrder()
- */
- public function generateCSR()
- {
- $domains = array_map(function ($dns) { return $dns['value']; }, $this->identifiers);
- if(in_array($this->basename, $domains))
- {
- $CN = $this->basename;
- }
- elseif(in_array('*.' . $this->basename, $domains))
- {
- $CN = '*.' . $this->basename;
- }
- else
- {
- $CN = $domains[0];
- }
-
- $dn = array(
- "commonName" => $CN
- );
-
- $san = implode(",", array_map(function ($dns) {
- return "DNS:" . $dns;
- }, $domains));
- $tmpConf = tmpfile();
- $tmpConfMeta = stream_get_meta_data($tmpConf);
- $tmpConfPath = $tmpConfMeta["uri"];
-
- fwrite($tmpConf,
- 'HOME = .
- RANDFILE = $ENV::HOME/.rnd
- [ req ]
- default_bits = 4096
- 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');
-
- $privateKey = openssl_pkey_get_private(file_get_contents($this->certificateKeys['private_key']));
- $csr = openssl_csr_new($dn, $privateKey, array('config' => $tmpConfPath, 'digest_alg' => 'sha256'));
- openssl_csr_export ($csr, $csr);
- return $csr;
- }
-
- /**
- * Checks, for redundancy, whether all authorizations are valid, and finalizes the order. Updates this LetsEncrypt Order instance with the new data.
- *
- * @param string $csr The Certificate Signing Request as a string. Can be a custom CSR. If empty, a CSR will be generated with the generateCSR() function.
- *
- * @return boolean Returns true if the finalize request was successful, false if not.
- */
- public function finalizeOrder($csr = '')
- {
- if($this->status == 'pending')
- {
- if($this->allAuthorizationsValid())
- {
- if(empty($csr)) $csr = $this->generateCSR();
- if(preg_match('~-----BEGIN\sCERTIFICATE\sREQUEST-----(.*)-----END\sCERTIFICATE\sREQUEST-----~s', $csr, $matches)) $csr = $matches[1];
- $csr = trim(LEFunctions::Base64UrlSafeEncode(base64_decode($csr)));
- $sign = $this->connector->signRequestKid(array('csr' => $csr), $this->connector->accountURL, $this->finalizeURL);
- $post = $this->connector->post($this->finalizeURL, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- $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 >= LECLient::LOG_STATUS) LEFunctions::log('Order for \'' . $this->basename . '\' finalized.', 'function finalizeOrder');
- return true;
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Not all authorizations are valid for \'' . $this->basename . '\'. Cannot finalize order.', 'function finalizeOrder');
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Order status for \'' . $this->basename . '\' is \'' . $this->status . '\'. Cannot finalize order.', 'function finalizeOrder');
- }
- return false;
- }
-
- /**
- * Gets whether the LetsEncrypt Order is finalized by checking whether the status is processing or valid. Keep in mind, a certificate is not yet available when the status still is processing.
- *
- * @return boolean Returns true if finalized, false if not.
- */
- public function isFinalized()
- {
- return ($this->status == 'processing' || $this->status == 'valid');
- }
-
- /**
- * Requests the certificate for this LetsEncrypt Order instance, after finalization. When the order status is still 'processing', the order will be polled max
- * four times with five seconds in between. If the status becomes 'valid' in the meantime, the certificate will be requested. Else, the function returns false.
- *
- * @return boolean Returns true if the certificate is stored successfully, false if the certificate could not be retrieved or the status remained 'processing'.
- */
- public function getCertificate()
- {
- $polling = 0;
- while($this->status == 'processing' && $polling < 4)
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Certificate for \'' . $this->basename . '\' being processed. Retrying in 5 seconds...', 'function getCertificate');
- sleep(5);
- $this->updateOrderData();
- $polling++;
- }
- if($this->status == 'valid' && !empty($this->certificateURL))
- {
- $get = $this->connector->get($this->certificateURL);
- if(strpos($get['header'], "200 OK") !== false)
- {
- if(preg_match_all('~(-----BEGIN\sCERTIFICATE-----[\s\S]+?-----END\sCERTIFICATE-----)~i', $get['body'], $matches))
- {
- if (isset($this->certificateKeys['certificate'])) file_put_contents($this->certificateKeys['certificate'], $matches[0][0]);
-
- if(count($matches[0]) > 1 && isset($this->certificateKeys['fullchain_certificate']))
- {
- $fullchain = $matches[0][0]."\n";
- for($i=1;$icertificateKeys['fullchain_certificate']), $fullchain);
- }
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Certificate for \'' . $this->basename . '\' saved', 'function getCertificate');
- return true;
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Received invalid certificate for \'' . $this->basename . '\'. Cannot save certificate.', 'function getCertificate');
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Invalid response for certificate request for \'' . $this->basename . '\'. Cannot save certificate.', 'function getCertificate');
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Order for \'' . $this->basename . '\' not valid. Cannot retrieve certificate.', 'function getCertificate');
- }
- return false;
- }
-
- /**
- * Revokes the certificate in the current LetsEncrypt Order instance, if existent. Unlike stated in the ACME draft, the certificate revoke request cannot be signed
- * with the account private key, and will be signed with the certificate private key.
- *
- * @param int $reason The reason to revoke the LetsEncrypt Order instance certificate. Possible reasons can be found in section 5.3.1 of RFC5280.
- *
- * @return boolean Returns true if the certificate was successfully revoked, false if not.
- */
- public function revokeCertificate($reason = 0)
- {
- if($this->status == 'valid')
- {
- 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');
-
- if(file_exists($certFile) && file_exists($this->certificateKeys['private_key']))
- {
- $certificate = file_get_contents($this->certificateKeys['certificate']);
- preg_match('~-----BEGIN\sCERTIFICATE-----(.*)-----END\sCERTIFICATE-----~s', $certificate, $matches);
- $certificate = trim(LEFunctions::Base64UrlSafeEncode(base64_decode(trim($matches[1]))));
-
- $sign = $this->connector->signRequestJWK(array('certificate' => $certificate, 'reason' => $reason), $this->connector->revokeCert);
- $post = $this->connector->post($this->connector->revokeCert, $sign);
- if(strpos($post['header'], "200 OK") !== false)
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Certificate for order \'' . $this->basename . '\' revoked.', 'function revokeCertificate');
- return true;
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Certificate for order \'' . $this->basename . '\' cannot be revoked.', 'function revokeCertificate');
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Certificate for order \'' . $this->basename . '\' not found. Cannot revoke certificate.', 'function revokeCertificate');
- }
- }
- else
- {
- if($this->log >= LECLient::LOG_STATUS) LEFunctions::log('Order for \'' . $this->basename . '\' not valid. Cannot revoke certificate.', 'function revokeCertificate');
- }
- return false;
- }
-}
-
-?>
diff --git a/LICENSE b/LICENSE
deleted file mode 100644
index e9f6e63..0000000
--- a/LICENSE
+++ /dev/null
@@ -1,21 +0,0 @@
-MIT License
-
-Copyright (c) 2018 Youri van Weegberg
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
diff --git a/LICENSE.md b/LICENSE.md
new file mode 100644
index 0000000..0bafa21
--- /dev/null
+++ b/LICENSE.md
@@ -0,0 +1,22 @@
+# The MIT License (MIT)
+
+Copyright (c) 2018 Youri van Weegberg
+Copyright (c) 2018 Paul Dixon
+
+> Permission is hereby granted, free of charge, to any person obtaining a copy
+> of this software and associated documentation files (the "Software"), to deal
+> in the Software without restriction, including without limitation the rights
+> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+> copies of the Software, and to permit persons to whom the Software is
+> furnished to do so, subject to the following conditions:
+>
+> The above copyright notice and this permission notice shall be included in
+> all copies or substantial portions of the Software.
+>
+> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+> THE SOFTWARE.
diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000..86246b3
--- /dev/null
+++ b/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,43 @@
+
+
+## Description
+
+Describe your changes in detail.
+
+## Motivation and context
+
+Why is this change required? What problem does it solve?
+
+If it fixes an open issue, please link to the issue here (if you write `fixes #num`
+or `closes #num`, the issue will be automatically closed when the pull is accepted.)
+
+## How has this been tested?
+
+Please describe in detail how you tested your changes.
+
+Include details of your testing environment, and the tests you ran to
+see how your change affects other areas of the code, etc.
+
+## Screenshots (if appropriate)
+
+## Types of changes
+
+What types of changes does your code introduce? Put an `x` in all the boxes that apply:
+- [ ] Bug fix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to change)
+
+## Checklist:
+
+Go over all the following points, and put an `x` in all the boxes that apply.
+
+Please, please, please, don't send your pull request until all of the boxes are ticked. Once your pull request is created, it will trigger a build on our [continuous integration](http://www.phptherightway.com/#continuous-integration) server to make sure your [tests and code style pass](https://help.github.com/articles/about-required-status-checks/).
+
+- [ ] I have read the **[CONTRIBUTING](CONTRIBUTING.md)** document.
+- [ ] My pull request addresses exactly one patch/feature.
+- [ ] I have created a branch for this patch/feature.
+- [ ] Each individual commit in the pull request is meaningful.
+- [ ] I have added tests to cover my changes.
+- [ ] If my change requires a change to the documentation, I have updated it accordingly.
+
+If you're unsure about any of these, don't hesitate to ask. We're here to help!
diff --git a/README.md b/README.md
index 8d27910..b4bf228 100644
--- a/README.md
+++ b/README.md
@@ -1,49 +1,80 @@
-# LEClient
-PHP LetsEncrypt client library for ACME v2. The aim of this client is to make an easy-to-use and integrated solution to create a LetsEncrypt-issued SSL/TLS certificate with PHP. The user has to have access to the web server or DNS management to be able to verify the domain is accessible/owned by the user.
+# PHP Certificate Toolbox
-## Current version
+[![Latest Version on Packagist][ico-version]][link-packagist]
+[![Software License][ico-license]](LICENSE.md)
+[![Build Status][ico-travis]][link-travis]
+[![Coverage Status][ico-scrutinizer]][link-scrutinizer]
+[![Quality Score][ico-code-quality]][link-code-quality]
-The current version is 1.1.0
+This is a LetsEncrypt client library for ACME v2, which allows for the automated
+creation of free SSL/TLS certificates using PHP. This includes support for wildcard
+certificates supported by LetsEncrypt since Feb 2018.
-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.
+While this includes a command line tool, the real intent of this library is to
+make it easy to integrate into existing PHP applications which need to issue
+certificates.
-## Getting Started
+See the [LetsEncrypt documentation](https://letsencrypt.org/docs/) for more
+information and documentation on LetsEncrypt and ACME.
-These instructions will get you started with this client library. Is you have any questions or find any problems, feel free to open an issue and I'll try to have a look at it.
+## Origins and roadmap
-Also have a look at the [LetsEncrypt documentation](https://letsencrypt.org/docs/) for more information and documentation on LetsEncrypt and ACME.
+This is based on the client developed by [Youri van Weegberg](https://github.com/yourivw/leclient),
+but improved as follows
-### Prerequisites
+* composer-installable
+* PSR-2 formatted
+* PSR-3 logger compatible
+* unit tests (some additional refactoring required to support this)
+* support for alternative storage backends
+* support for verifying DNS challenges using DNS-over-HTTPS
-The minimum required PHP version is 7.1.0 due to the implementation of ECDSA. Version 1.0.0 does still work with PHP 5.2 since it is not yet compatible with ECDSA, and will be kept available, but will not be maintained.
-This client also depends on cURL and OpenSSL.
+## Prerequisites
-### Installing
+The minimum required PHP version is 7.1.0 due to the implementation of ECDSA.
-Download and install the LEClient folder and examples wherever you want to install it. You can include the library by adding the following:
-```php
-require_once('LEClient/LEClient.php');
-```
+This client also depends on OpenSSL.
-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:
-```php
-ini_set('max_execution_time', 120); // Maximum execution time in seconds.
+
+## Install
+
+Via Composer
+
+``` bash
+$ composer require lordelph/php-certificate-toolbox
```
## Usage
-The basic functions and its necessary arguments are shown here. An extended description is included in each class.
+The basic functions and its necessary arguments are shown here. An extended description
+is included in each class.
-
+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:
+
+```php
+ini_set('max_execution_time', 120); // Maximum execution time in seconds.
+```
Initiating the client:
+
```php
-$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, LEClient::LOG_STATUS); // Initiating a LEClient and log status messages (LOG_DEBUG for full debugging).
+use Elphin\LEClient;
+
+// Initiating a basic LEClient with an array of string e-mail address(es).
+$client = new LEClient($email);
+
+// Initiating a LECLient and use the LetsEncrypt staging URL.
+$client = new LEClient($email, true);
+
+// Initiating a LEClient and log status messages (LOG_DEBUG for full debugging).
+$client = new LEClient($email, true, LEClient::LOG_STATUS);
```
-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.
+
+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.
@@ -94,11 +125,24 @@ LEFunctions::createhtaccess($directory); // Created a simple .htaccess f
## Authorization challenges
-LetsEncrypt (ACME) performs authorizations on the domains you want to include on your certificate, to verify you actually have access to the specific domain. Therefore, when creating an order, an authorization is added for each domain. If a domain has recently (in the last 30 days) been verified by your account, for example in another order, you don't have to verify again. At this time, a domain can be verified by a HTTP request to a file (http-01) or a DNS TXT record (dns-01). The client supplies the necessary data for the chosen verification by the call to getPendingAuthorizations(). Since creating a file or DNS record differs for every server, this is not implemented in the client. After the user has fulfilled the challenge requirements, a call has to be made to verifyPendingOrderAuthorization(). This client will first verify the challenge with checkHTTPChallenge() or checkDNSChallenge() by itself, before it is starting the verification by LetsEncrypt. Keep in mind, a wildcard domain can only be verified with a DNS challenge. An example for both challenges is shown below.
+LetsEncrypt (ACME) performs authorizations on the domains you want to include on your
+certificate, to verify you actually have access to the specific domain. Therefore, when
+creating an order, an authorization is added for each domain. If a domain has recently
+(in the last 30 days) been verified by your account, for example in another order, you
+don't have to verify again. At this time, a domain can be verified by a HTTP request to
+a file (http-01) or a DNS TXT record (dns-01). The client supplies the necessary data
+for the chosen verification by the call to `getPendingAuthorizations()`. Since creating a
+file or DNS record differs for every server, this is not implemented in the client.
+After the user has fulfilled the challenge requirements, a call has to be made to
+`verifyPendingOrderAuthorization()`. This client will first verify the challenge with
+`checkHTTPChallenge()` or `checkDNSChallenge()` by itself, before it is starting the
+verification by LetsEncrypt. Keep in mind, a wildcard domain can only be verified with
+a DNS challenge. An example for both challenges is shown below.
### HTTP challenge
For this example, we assume there is one domain left to verify.
+
```php
$pending = $order->getPendingAuthorizations(LEOrder::CHALLENGE_TYPE_HTTP);
```
@@ -119,11 +163,16 @@ For a successful verification, a request will be made to the following URL:
```
http://test.example.org/.well-known/acme-challenge/A8Q1DAVcd_k_oKAC0D_y4ln2IWrRX51jmXnR9UMMtOb
```
-The content of this file should be set to the content in the array above. The user should create this file before it can verify the authorization.
+The content of this file should be set to the content in the array above. The user should
+create this file before it can verify the authorization.
### DNS challenge
-For this example, we assume there are two domains left to verify. One is a wildcard domain. The second domain in this example is added for demonstration purposes. Adding a subdomain to the certificate which is also already covered by the wildcard domain is does not offer much added value.
+For this example, we assume there are two domains left to verify. One is a wildcard domain.
+The second domain in this example is added for demonstration purposes. Adding a subdomain to
+the certificate which is also already covered by the wildcard domain is does not offer much
+added value.
+
```php
$pending = $order->getPendingAuthorizations(LEOrder::CHALLENGE_TYPE_DNS);
```
@@ -152,22 +201,97 @@ For a successful verification, DNS records should be created as follows:
| \_acme-challenge.example.org | 60 | TXT | FV5HgbpjIYe1x9MkPI81Nffo2oA-Jo2S88gCL7-Ky5P |
| \_acme-challenge.test.example.org | 60 | TXT | WM5YIsgaZQv1b9DbRZ81EwCf2fi-Af2JlgxTC7-Up5D |
-The TTL value can be set higher if wanted or necessary, I prefer to keep it as low as possible for this purpose. To make sure the verification is successful, it would be advised to run a script using DNS challenges in two parts, with a certain amount of time in between to allow for the DNS record to update. The user himself should make sure to set this DNS record before the record can be verified.
-The DNS record name also depends on your provider, therefore getPendingAuthorizations() does not give you a ready-to-use record name. Some providers only accept a name like `_acme-challenge`, without the top domain name, for `_acme-challenge.example.org`. Some providers accept (require?) a full name like shown above.
+The TTL value can be set higher if wanted or necessary, I prefer to keep it as low as possible for
+this purpose. To make sure the verification is successful, it would be advised to run a script
+using DNS challenges in two parts, with a certain amount of time in between to allow for the DNS
+record to update. The user himself should make sure to set this DNS record before the record can
+be verified.
-*A wildcard domain, like `*.example.org`, will be verified as `example.org`, as shown above. This means the DNS record name should be `_acme-challenge.example.org`*
+The DNS record name also depends on your provider, therefore `getPendingAuthorizations()` does
+not give you a ready-to-use record name. Some providers only accept a name like `_acme-challenge`,
+without the top domain name, for `_acme-challenge.example.org`. Some providers accept (require?)
+a full name like shown above.
+
+*A wildcard domain, like `*.example.org`, will be verified as `example.org`, as shown above.
+This means the DNS record name should be `_acme-challenge.example.org`*
## Full example
-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.
+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.
+
+
+## Change log
+
+Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
+
+## Testing
+
+Unit tests are executed as follows:
+
+``` bash
+$ composer test
+```
+
+The test suite includes some integration tests with external dependencies, e.g. verifying
+that each supported DNS-over-HTTP service works as expected. The full test suite can be
+run with
+
+``` bash
+$ composer test-all
+```
+
+## Contributing
+
+Please see [CONTRIBUTING](CONTRIBUTING.md) and [CODE_OF_CONDUCT](CODE_OF_CONDUCT.md) for details.
-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.
## Security
-Security is an important subject regarding SSL/TLS certificates, of course. Since this client is a PHP script, it is likely this code is running on a web server. It is obvious that your private key, stored on your web server, should never be accessible from the web.
-When the client created the keys directory for the first time, it will store a .htaccess file in this directory, denying all visitors. Always make sure yourself your keys aren't accessible from the web! I am in no way responsible if your private keys go public. If this does happen, the easiest solution is to change your account keys (described above) or deactivate your account and create a new one. Next, create a new certificate.
+Security is an important subject regarding SSL/TLS certificates, of course. Since this client is
+a PHP script, it is likely this code is running on a web server. It is obvious that your private
+key, stored on your web server, should never be accessible from the web.
+
+When the client created the keys directory for the first time, it will store a .htaccess file in
+this directory, denying all visitors. Always make sure yourself your keys aren't accessible from
+the web! I am in no way responsible if your private keys go public. If this does happen, the
+easiest solution is to change your account keys (described above) or deactivate your account and
+create a new one. Next, create a new certificate.
+
+If you discover any security related issues, please email paul@elphin.com instead of using the
+issue tracker.
+
+## Credits
+
+- [Paul Dixon][link-author] Refactoring inc unit tests and storage interface
+- [Youri van Weegberg][link-author2] Original PHP ACME2 client on which this is based
+- [wutno][link-author3] DNS-over-HTTPS support
+
+- [All Contributors][link-contributors]
## License
-This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.
+The MIT License (MIT). Please see [License File](LICENSE.md) for more information.
+
+[ico-version]: https://img.shields.io/packagist/v/zwartpet/php-certificate-toolbox.svg?style=flat-square
+[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square
+[ico-travis]: https://img.shields.io/travis/zwartpet/php-certificate-toolbox/master.svg?style=flat-square
+[ico-scrutinizer]: https://img.shields.io/scrutinizer/coverage/g/zwartpet/php-certificate-toolbox.svg?style=flat-square
+[ico-code-quality]: https://img.shields.io/scrutinizer/g/zwartpet/php-certificate-toolbox.svg?style=flat-square
+
+[link-packagist]: https://packagist.org/packages/zwartpet/php-certificate-toolbox
+[link-travis]: https://travis-ci.org/zwartpet/php-certificate-toolbox
+[link-scrutinizer]: https://scrutinizer-ci.com/g/zwartpet/php-certificate-toolbox/code-structure
+[link-code-quality]: https://scrutinizer-ci.com/g/zwartpet/php-certificate-toolbox
+[link-downloads]: https://packagist.org/packages/zwartpet/php-certificate-toolbox
+[link-author]: https://github.com/zwartpet
+[link-author2]: https://github.com/lordelph
+[link-author3]:https://github.com/yourivw
+[link-author3]:https://github.com/GXTX
+[link-contributors]: ../../contributors
diff --git a/composer.json b/composer.json
index 1bcc648..db150b4 100644
--- a/composer.json
+++ b/composer.json
@@ -1,14 +1,68 @@
{
- "name": "yourivw/LEClient",
+ "name": "zwartpet/php-certificate-toolbox",
"type": "library",
- "description": "PHP LetsEncrypt client library for ACME v2",
- "repositories": [
+ "description": "ACME v2 client for Let's Encrypt",
+ "keywords": [
+ "Lets Encrypt",
+ "ACME",
+ "LE",
+ "Certificate"
+ ],
+ "homepage": "https://github.com/zwartpet/php-certificate-toolbox",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "John Zwarthoed",
+ "homepage": "https://github.com/zwartpet",
+ "role": "Developer"
+ },
+ {
+ "name": "Paul Dixon",
+ "email": "paul@elphin.com",
+ "homepage": "http://blog.dixo.net",
+ "role": "Developer"
+ },
{
- "url": "https://github.com/yourivw/LEClient.git",
- "type": "git"
+ "name": "Youri van Weegberg",
+ "homepage": "https://github.com/yourivw/LEClient",
+ "role": "Developer"
}
],
"require": {
- "yourivw/LEClient": "^1.1.0"
+ "php": "~7.0",
+ "ext-openssl": "*",
+ "guzzlehttp/guzzle": "~6.0",
+ "psr/log": "^1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit" : ">=5.4.3",
+ "squizlabs/php_codesniffer": "^2.3"
+ },
+ "suggest": {
+ "psr/log-implementation": "A PSR-3 compatible logger is recommended for troubleshooting"
+ },
+ "autoload": {
+ "psr-4": {
+ "Zwartpet\\PHPCertificateToolbox\\": "src"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "Zwartpet\\PHPCertificateToolbox\\": "tests"
+ }
+ },
+ "scripts": {
+ "test": "phpunit --exclude-group integration",
+ "test-all": "phpunit",
+ "check-style": "phpcs -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests",
+ "fix-style": "phpcbf -p --standard=PSR2 --runtime-set ignore_errors_on_exit 1 --runtime-set ignore_warnings_on_exit 1 src tests"
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "config": {
+ "sort-packages": true
}
}
diff --git a/exampleDNSInit.php b/exampleDNSInit.php
deleted file mode 100644
index 464a46b..0000000
--- a/exampleDNSInit.php
+++ /dev/null
@@ -1,33 +0,0 @@
-getOrCreateOrder($basename, $domains);
-// Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations.
-if(!$order->allAuthorizationsValid())
-{
- // Get the DNS challenges from the pending authorizations.
- $pending = $order->getPendingAuthorizations(LEOrder::CHALLENGE_TYPE_DNS);
- // Walk the list of pending authorization DNS challenges.
- if(!empty($pending))
- {
- foreach($pending as $challenge)
- {
- // For the purpose of this example, a fictitious functions creates or updates the ACME challenge DNS record for this domain.
- setDNSRecord($challenge['identifier'], $challenge['DNSDigest'])
- }
- }
-}
-?>
\ No newline at end of file
diff --git a/exampleDNSFinish.php b/examples/exampleDNSFinish.php
similarity index 73%
rename from exampleDNSFinish.php
rename to examples/exampleDNSFinish.php
index e3061d0..7d4227f 100644
--- a/exampleDNSFinish.php
+++ b/examples/exampleDNSFinish.php
@@ -1,8 +1,10 @@
getOrCreateOrder($basename, $domains);
// Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations.
if(!$order->allAuthorizationsValid())
@@ -37,5 +47,12 @@
if(!$order->isFinalized()) $order->finalizeOrder();
// Check whether the order has been finalized before we can get the certificate. If finalized, get the certificate.
if($order->isFinalized()) $order->getCertificate();
+
+ //finally, here's how we revoke
+ //echo "REVOKING...\n";
+ //$order->revokeCertificate();
}
-?>
\ No newline at end of file
+
+
+echo "\nDiagnostic logs\n";
+$logger->dumpConsole();
diff --git a/examples/exampleDNSInit.php b/examples/exampleDNSInit.php
new file mode 100644
index 0000000..9b5aa0a
--- /dev/null
+++ b/examples/exampleDNSInit.php
@@ -0,0 +1,59 @@
+getOrCreateOrder($basename, $domains);
+// Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations.
+ if (!$order->allAuthorizationsValid()) {
+ // Get the DNS challenges from the pending authorizations.
+ $pending = $order->getPendingAuthorizations(LEOrder::CHALLENGE_TYPE_DNS);
+ // Walk the list of pending authorization DNS challenges.
+ if (!empty($pending)) {
+ foreach ($pending as $challenge) {
+ // For the purpose of this example, a fictitious functions creates or updates the ACME challenge DNS
+ // record for this domain.
+ //setDNSRecord($challenge['identifier'], $challenge['DNSDigest']);
+ printf(
+ "DNS Challengage identifier = %s digest = %s\n",
+ $challenge['identifier'],
+ $challenge['DNSDigest']
+ );
+ }
+ }
+ }
+}
+catch (\Exception $e) {
+ echo $e->getMessage()."\n";
+ echo $e->getTraceAsString()."\n";
+
+ echo "\nDiagnostic logs\n";
+ $logger->dumpConsole();
+ exit;
+}
+
+echo "\nDiagnostic logs\n";
+$logger->dumpConsole();
+
diff --git a/exampleHTTP.php b/examples/exampleHTTP.php
similarity index 80%
rename from exampleHTTP.php
rename to examples/exampleHTTP.php
index 4140f03..e1617a3 100644
--- a/exampleHTTP.php
+++ b/examples/exampleHTTP.php
@@ -1,8 +1,10 @@
getOrCreateOrder($basename, $domains);
// Check whether there are any authorizations pending. If that is the case, try to verify the pending authorizations.
if(!$order->allAuthorizationsValid())
@@ -25,7 +31,8 @@
{
foreach($pending as $challenge)
{
- // Define the folder in which to store the challenge. For the purpose of this example, a fictitious path is set.
+ // Define the folder in which to store the challenge. For the purpose of this example, a fictitious path is
+ // set.
$folder = '/path/to/' . $challenge['identifier'] . '/.well-known/acme-challenge/';
// Check if that directory yet exists. If not, create it.
if(!file_exists($folder)) mkdir($folder, 0777, true);
@@ -44,4 +51,6 @@
// Check whether the order has been finalized before we can get the certificate. If finalized, get the certificate.
if($order->isFinalized()) $order->getCertificate();
}
-?>
\ No newline at end of file
+
+echo "\nDiagnostic logs\n";
+$logger->dumpConsole();
\ No newline at end of file
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
new file mode 100644
index 0000000..0b2de5f
--- /dev/null
+++ b/phpunit.xml.dist
@@ -0,0 +1,29 @@
+
+
+
+
+ tests
+
+
+
+
+ src/
+
+
+
+
+
+
+
+
+
+
diff --git a/src/CertificateStorageInterface.php b/src/CertificateStorageInterface.php
new file mode 100644
index 0000000..9f323fd
--- /dev/null
+++ b/src/CertificateStorageInterface.php
@@ -0,0 +1,116 @@
+baseURI = self::DNS_GOOGLE;
+ } else {
+ $this->baseURI = $baseURI;
+ }
+
+ $this->client = new Client([
+ 'base_uri' => $this->baseURI
+ ]);
+ }
+
+ public function checkChallenge($domain, $requiredDigest) : bool
+ {
+ $hostname = '_acme-challenge.' . str_replace('*.', '', $domain);
+
+ $records = $this->get($hostname, 'TXT');
+ if ($records->Status == 0) {
+ foreach ($records->Answer as $record) {
+ if ((rtrim($record->name, ".") == $hostname) &&
+ ($record->type == 16) &&
+ (trim($record->data, '"') == $requiredDigest)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * @param string $name
+ * @param string $type per experimental spec this can be string OR int, we force string
+ * @return \stdClass
+ */
+ public function get(string $name, string $type) : \stdClass
+ {
+ $query = [
+ 'query' => [
+ 'name' => $name,
+ 'type' => $type,
+ 'edns_client_subnet' => '0.0.0.0/0', //disable geotagged dns results
+ 'ct' => 'application/dns-json', //cloudflare requires this
+ ],
+ 'headers' => [
+ 'Accept' => 'application/dns-json'
+ ]
+ ];
+
+ try {
+ $response = $this->client->get(null, $query);
+ } catch (BadResponseException $e) {
+ throw new RuntimeException("GET {$this->baseURI} failed", 0, $e);
+ }
+
+ $decode = json_decode($response->getBody());
+
+ if (json_last_error() !== JSON_ERROR_NONE) {
+ throw new RuntimeException(
+ 'Attempted to decode expected JSON response, however server returned something unexpected.'
+ );
+ }
+
+ return $decode;
+ }
+}
diff --git a/src/DNSValidator/DNSValidatorInterface.php b/src/DNSValidator/DNSValidatorInterface.php
new file mode 100644
index 0000000..2c1db24
--- /dev/null
+++ b/src/DNSValidator/DNSValidatorInterface.php
@@ -0,0 +1,23 @@
+logs[] = [$level, $message, $context];
+ }
+
+ public function dumpConsole($useColours = true)
+ {
+ $colours = [
+ 'alert' => "\e[97m\e[41m",
+ 'emergency' => "\e[97m\e[41m",
+ 'critical' => "\e[97m\e[41m",
+ 'error' => "\e[91m",
+ 'warning' => "\e[93m",
+ 'notice' => "\e[96m",
+ 'info' => "\e[92m",
+ 'debug' => "\e[2m",
+ ];
+
+ $reset = $useColours ? "\e[0m" : '';
+
+ foreach ($this->logs as $log) {
+ $col = $useColours ? $colours[$log[0]] : '';
+ echo $col . $log[0] . ': ' . $this->interpolateMessage($log[1], $log[2]) . $reset . "\n";
+ }
+ }
+
+ public function dumpHTML($echo = true)
+ {
+ $html = '