diff --git a/.github/workflows/build-docker-master.yaml b/.github/workflows/build-docker-master.yaml new file mode 100644 index 00000000..ea000eb1 --- /dev/null +++ b/.github/workflows/build-docker-master.yaml @@ -0,0 +1,42 @@ +name: Continuous integration (release) +on: + push: + branches: + - master +jobs: + build_container: + name: Build container + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: box + - uses: ramsey/composer-install@v3 + - name: Build PHAR + run: box compile + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.3.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6.0.0 + with: + context: build/ + file: Dockerfile + push: true + load: true + tags: "ghcr.io/${{ github.repository }}/acmephp:master-${{ github.sha }}" + - name: Confirm that we can run ACME php via docker + run: docker run --rm ghcr.io/${{ github.repository }}/acmephp:master-${{ github.sha }} +permissions: + packages: write + contents: read diff --git a/.github/workflows/gitsplit.yaml b/.github/workflows/gitsplit.yaml new file mode 100644 index 00000000..388e8a7a --- /dev/null +++ b/.github/workflows/gitsplit.yaml @@ -0,0 +1,22 @@ +name: gitsplit +on: + push: + branches: + - master + tags: + - '*' + release: + types: [published] + +jobs: + gitsplit: + runs-on: ubuntu-latest + steps: + - name: checkout + run: git clone https://github.com/acmephp/acmephp /home/runner/work/acmephp/acmephp && cd /home/runner/work/acmephp/acmephp + - name: Split repositories + uses: docker://jderusse/gitsplit:latest + with: + args: gitsplit + env: + GH_TOKEN: ${{ secrets.PRIVATE_TOKEN }} diff --git a/.github/workflows/pr-phar.yaml b/.github/workflows/pr-phar.yaml new file mode 100644 index 00000000..e4e986f1 --- /dev/null +++ b/.github/workflows/pr-phar.yaml @@ -0,0 +1,32 @@ +name: Create phar +on: + pull_request_target: +jobs: + create_phar: + name: Create phar + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: box + - uses: ramsey/composer-install@v3 + - name: Build PHAR + run: box compile + - uses: actions/upload-artifact@v4 + id: artifact-upload + with: + name: amcephp.phar + path: build/acmephp.phar + if-no-files-found: error + overwrite: true + - uses: mshick/add-pr-comment@v2 + with: + message: | + We have created a phar file for testing, find it here: ${{ steps.artifact-upload.outputs.artifact-url }} diff --git a/.github/workflows/test-build.yaml b/.github/workflows/test-build.yaml new file mode 100644 index 00000000..e2290e95 --- /dev/null +++ b/.github/workflows/test-build.yaml @@ -0,0 +1,115 @@ +name: Test and build + +on: + pull_request: ~ + push: + branches: + - master + +jobs: + php-cs: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + steps: + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - uses: actions/checkout@master + + - name: Install php-cs-fixer + run: wget https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v3.62.0/php-cs-fixer.phar -q + + - name: Check coding style + run: php php-cs-fixer.phar fix --dry-run --diff + + test_docker_build: + name: Build container + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + tools: box + - uses: ramsey/composer-install@v3 + - name: Build PHAR + run: box compile + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3.3.0 + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + uses: docker/build-push-action@v6.0.0 + with: + context: build/ + file: Dockerfile + push: false + load: true + tags: ghcr.io/${{ github.repository }}/acmephp:master-${{ github.sha }} + - name: Confirm that we can run ACME php via docker + run: docker run --rm ghcr.io/${{ github.repository }}/acmephp:master-${{ github.sha }} + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + + - uses: actions/checkout@v4 + + - name: Install Composer dependencies + run: | + composer update --prefer-dist --no-interaction + + - name: Run PHPStan + run: vendor/bin/phpstan analyse + + ci: + name: Test PHP ${{ matrix.php-version }} ${{ matrix.pebble_mode }} ${{ matrix.name }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php-version: ["8.3"] + composer-flags: [""] + name: [""] + pebble_mode: [""] + include: + - php-version: 8.3 + composer-flags: "--prefer-lowest" + name: "(prefer lowest dependencies)" + - php-version: 8.3 + composer-flags: "--prefer-lowest" + name: "(prefer lowest dependencies - EAB)" + pebble_mode: eab + - php-version: 8.3 + name: "(EAB)" + pebble_mode: eab + env: + PEBBLE_MODE: "${{ matrix.pebble_mode }}" + + steps: + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php-version }} + + - uses: actions/checkout@v4 + + - name: Install Composer dependencies + run: | + composer update --prefer-dist --no-interaction ${{ matrix.composer-flags }} + + - name: Preparing tests + run: ./tests/setup.sh + + - name: Running tests + run: ./tests/run.sh diff --git a/.gitignore b/.gitignore index 3cda68c4..a3c77191 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /box.json composer.lock vendor/ -.php_cs.cache +.php-cs-fixer.cache doc/.couscous .subsplit +.phpunit.result.cache diff --git a/.gitsplit.yml b/.gitsplit.yml index 2c60b22e..fb72d98d 100644 --- a/.gitsplit.yml +++ b/.gitsplit.yml @@ -1,8 +1,5 @@ # See https://github.com/jderusse/docker-gitsplit -# Path to a cache directory Used to speed up the split over time by reusing git's objects -cache_url: "/cache/gitsplit" - splits: - prefix: "src/Core" target: "https://${GH_TOKEN}@github.com/acmephp/core.git" diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..be3821f3 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,26 @@ + + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__.'/bin') + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') +; + +return (new PhpCsFixer\Config()) + ->setFinder($finder) + ->setRules([ + '@Symfony' => true, + 'array_syntax' => ['syntax' => 'short'], + 'phpdoc_annotation_without_dot' => false, + 'header_comment' => ['header' => $header], + ]) +; diff --git a/.php_cs b/.php_cs deleted file mode 100644 index acd8b667..00000000 --- a/.php_cs +++ /dev/null @@ -1,40 +0,0 @@ - - -For the full copyright and license information, please view the LICENSE -file that was distributed with this source code. -EOF; - -$config = PhpCsFixer\Config::create() - ->setRiskyAllowed(true) - ->setRules([ - '@Symfony' => true, - '@Symfony:risky' => true, - 'array_syntax' => ['syntax' => 'short'], - 'ordered_imports' => true, - 'header_comment' => ['header' => $header], - 'linebreak_after_opening_tag' => true, - 'modernize_types_casting' => true, - 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced'], - 'no_superfluous_elseif' => true, - 'no_useless_else' => true, - 'phpdoc_order' => true, - 'psr4' => true, - 'simplified_null_return' => true, - 'no_useless_return' => true, - 'strict_comparison' => true, - 'yoda_style' => true, - ]) - ->setFinder( - PhpCsFixer\Finder::create() - ->in(__DIR__.'/') - ->exclude('vendor') - ->name('*.php') - ) -; - -return $config; diff --git a/.rmt.yml b/.rmt.yml deleted file mode 100644 index 6a18a5f0..00000000 --- a/.rmt.yml +++ /dev/null @@ -1,23 +0,0 @@ -vcs: - name: git - sign-tag: true - -version-generator: - name: semantic - allow-label: true - -version-persister: vcs-tag - -prerequisites: - display-last-changes: ~ - -pre-release-actions: - changelog-update: - format: simple - file: CHANGELOG.md - dump-commits: true - vcs-commit: ~ - -post-release-actions: - vcs-publish: - ask-confirmation: true diff --git a/.sensiolabs.yml b/.sensiolabs.yml deleted file mode 100644 index 23e1fe78..00000000 --- a/.sensiolabs.yml +++ /dev/null @@ -1,3 +0,0 @@ -commit_failure_conditions: - - "project.severity.critical > 0" - - "project.severity.major > 0" diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6a786133..00000000 --- a/.travis.yml +++ /dev/null @@ -1,83 +0,0 @@ -sudo: false - -language: php - -branches: - only: - - master - -notifications: - email: false - -.template_phpunit: &phpunit - stage: test - services: - - docker - before_install: - - stty cols 120 - - phpenv config-rm xdebug.ini || echo "xdebug not available" - - ./tests/setup.sh - install: composer install --no-interaction --no-progress --ansi - script: ./tests/run.sh - env: - SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: 1 - cache: - directories: - - $HOME/.composer/cache/files - -.template_phpunit_low: &phpunit_low - <<: *phpunit - install: - - composer require --dev "sebastian/comparator:^2.0" - - composer update --no-interaction --no-progress --ansi --prefer-lowest --prefer-stable - env: - COMMENT: "low deps" - SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: 1 - -.template_phpunit_high: &phpunit_high - <<: *phpunit - install: composer update --no-interaction --no-progress --ansi - env: - COMMENT: "high deps" - SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT: 1 - -jobs: - include: - - stage: coding-style - php: 7.2 - install: - - wget https://github.com/FriendsOfPHP/PHP-CS-Fixer/releases/download/v2.16.1/php-cs-fixer.phar -q - script: - - php php-cs-fixer.phar fix --dry-run --diff - - - <<: *phpunit - dist: trusty - php: 5.5 - - <<: *phpunit - php: 5.6 - - <<: *phpunit_low - php: 7.0 - - <<: *phpunit - php: 7.1 - - <<: *phpunit - php: 7.2 - - <<: *phpunit - php: 7.3 - - <<: *phpunit_high - php: 7.4 - - - stage: split - php: 7.4 - cache: - directories: - - $HOME/.gitsplit/cache - if: branch = master AND fork = false - install: - - docker pull jderusse/gitsplit:2.0 - - git config remote.origin.fetch "+refs/*:refs/*" - - git config remote.origin.mirror true - - git fetch --unshallow || true - env: - COMMENT: "split repo" - script: - - docker run --rm -t -e GH_TOKEN -v "$HOME/.gitsplit/cache":/cache/gitsplit -v ${PWD}:/srv jderusse/gitsplit:2.0 gitsplit --ref "${TRAVIS_BRANCH}"; diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f8bdf4..004e9c23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,165 +1,293 @@ -15/01/2020 22:42 1.2.0 Release version 1.2.0 -d031223 Merge pull request #185 from miranovy/master -24b8575 Fix getIssuerCertificate return type -df8f156 Merge pull request #183 from jderusse/split-request -b86d2d6 Merge pull request #184 from miranovy/master -ec1bbba fix the wrong return type -028470a fix the wrong return type -25a12e3 Split Sign and Request -937a2f7 Merge pull request #182 from pauladams8/master -c592b85 added orderNotReady error type -0d09084 Merge pull request #180 from jderusse/fix-test -f0e9422 Add php 7.3 and 7.4 -6b65f1a Merge pull request #164 from trustocean/feat-install-aliyun-cdn -d060f0c style ci, no spacing near dot -222f184 重名时阿里云会报错 -51da67e Merge branch 'master' of https://github.com/acmephp/acmephp into feat-install-aliyun-cdn -9203dec Merge pull request #163 from trustocean/feat-install-aliyun-waf -ce4bb92 style ci -45037e1 Merge branch 'master' of https://github.com/acmephp/acmephp into feat-install-aliyun-cdn -dcd1ad7 fixbug -bce7829 fixbug -3292b41 fix -2400302 remove wrong conf -0501e56 register in services -505d4c2 stash -0d522f7 Update InstallAliyunWafAction.php -91049e3 Remove try...catch wrapper -e9fefc3 stash -2d88030 Merge pull request #161 from jderusse/deprecate-commands -723de09 style ci -1a17f6e fix style ci -40558ea close #17 -05a2f39 fix -8541342 statsh -d865632 Merge pull request #162 from aik099/gandi-dns-solver-feat -83e47d3 Removed unused code -62d9b1b CS fixes -9eeb20a Adding Gandi.Net DNS solver class -8bae348 Deprecate commands in favor of run -9ef2916 Merge pull request #153 from elliotfehr/missed-memleak -c1271c0 free openssl resource after reading -b276743 Merge pull request #151 from elliotfehr/openssl-mem-leak -1effe3e Merge branch 'master' into openssl-mem-leak -58ee1e6 Merge pull request #152 from jderusse/fix-cs -2071f9d Fix CS -bb55db6 free the key resource after reading +# CHANGELOG + +## 2.2.0 (not released yet) + +> [!NOTE] +> From now on, a particular attention will be given to provide a nice changelog. + +### Features + +* Add support for lcobucci/jwt ^5.3 +* Add support for guzzlehttp/psr7 ^2.4 +* Add support for symfony ^7.1 +* Add support for psr/http-message ^2 +* Add support for psr/container ^2 +* Add support for psr/log ^2 || ^3 +* Add support for consolidation/self-update ^2.2 || ^3 + +### End of support + +* Drop support for PHP < 8.3 +* Drop support for Symfony < 5.4, and 6.0, 6.1, 6.2, 6.3 +* Drop support for lcobucci/jwt < 5.3 +* Drop support for guzzlehttp/psr7 < 2 +* Upgrade from FlySystem v1 to v3 +* Drop support for monolog < 3 +* Drop support for psr/log < 2 + +### Internal + +* Analyse code with PHPStan + +## 07/06/2022 22:41 2.1.0 Add compatibility for PHP 8.0/8.1, Symfony 6 and other improvements + +* 8a8a975 Merge pull request #263 from acmephp/core-get-order +* 95c17a4 Add Core\AcmeClient::reloadOrder method +* 7bac887 Merge pull request #257 from mgriego/core-deps-and-bug-fixes +* 371c905 Merge pull request #251 from W0rma/remove-swiftmailer +* 09e4bea Serialize embedded authorization challenges when serializing order objects. +* d345ffe Allow for newer dependencies and fix a couple of issues in AcmePhp\Core. +* 8be2586 Merge pull request #262 from acmephp/upgrade-phpcs +* 03d14b3 Remove unused swiftmailer dependency +* 74b71f5 Merge pull request #254 from W0rma/fix-badges +* 182d539 Fix CS +* 2ae3edf Upgrade PHP-CS-Fixer +* d685b93 Fix badges in README +* c7fb74e Merge pull request #233 from tbickley-mediabowl/issue/197_Monolog2 +* 043fd6c Allow Monolog version 2 +* e23b888 Merge pull request #261 from acmephp/php81 +* 2cfc866 Allow Symfony 6 +* 2755d98 Add PHP 8.1 tests and fix deprecations +* 22df0d1 feature #250 Fix tests on PHP 8.0/8.1 (HansAdema) +* 7621356 Fix tests on PHP 8.0/8.1 +* 2299602 Update README.md +* 2e43e83 Merge pull request #231 from piotrantosik/feature/core-deps +* 192872c Allow lcobucci/jwt ^4.0 in core package + +## 03/02/2021 22:29 2.0.1 Fix requirements + +* 8efc61a Fix CI config +* 8b26916 Merge pull request #223 from piotrantosik/patch-1 +* f836e0c Merge pull request #224 from jackdpeterson/expand_composer_deps +* b50c730 expand compatibility range for lcobucci/jwt to include both ^3.3 as well as ^4.0. +* f1fbaf1 Require acmephp/ssl 2.0 + +## 14/12/2020 20:10 2.0.0 v2.0 + +* 2683496 Fix Box config and bump version +* 4f56592 Merge pull request #212 from acmephp/2.0 +* 65a0ac1 Fix tests and CS +* 8dc7923 Add alternate certificate tests +258ea5c Add option that allows a client to download the alternate certificate link instead * of the default one +* 7e96853 Fix encoding issue +* e3550c5 Allow to configure directly EAB credentials +* c2bf09b Finalize EAB support +* 8f10906 Adapt tests +* 12533ba Add EAB structure +* 7fa7a2a Add EAB test +* 701b864 Bump minimum version of Symfony +* 5183019 Remove legacy tests +* 5ef6681 Migrate to Github Actions +* 3fc8c60 Remove deprecated features +* ccbba76 Fix CS +* 2a473a8 Migrate acmephp/cli to use typehints +* 7d6b566 Fix tests +* ab68cac Bump PHP-CS-Fixer version +* c061e3a Migrate acmephp/core to use typehints +* b342a86 Migrate acmephp/ssl to use typehints +* be0a5b1 Fix subpackages composer.json +* e51784a Remove deprecated commands, improve tests on Run command and merge v2 interfaces +* e2be9c3 Upgrade dependencies and drop support for PHP <7.2 +* 2330ef6 Bump version + +## 13/12/2020 23:03 1.3.0 v1.3.0 + +* 5d37fb1 Merge pull request #218 from acmephp/do-not-verify-https-http-vaidator +* fe50cf6 Merge pull request #217 from acmephp/openssl-php8 +* e849f30 Do not check HTTPS certificate validity in HttpValidator +* 38a9a5f Fix openssl_free_key deprecation notice in PHP 8 +* cb1eae4 Merge pull request #219 from acmephp/fix-tests +* 5514e91 Fix 1.0 CI +* db4d497 Merge pull request #192 from acmephp/handle-processing +* 965c6b6 Fix Box config for latest Box version +* 6968927 Merge pull request #208 from InfinityFreeHosting/guzzle-7 +* 6ec9c47 Support both Guzzle 6.x and 7.x +* 9565469 Merge pull request #204 from p-seven-v/add-rejected-identifier-exception +* a9ed8ac Fixed quotes +* c0d3f0d Added more exceptions +* 3749f96 Reordered alphabetically +* 47c2139 Added class comment +* 765dd86 Added RejectedIdentifierServerException +* 852d90c Handle processing status case +* 5b07014 Merge pull request #193 from acmephp/update-ci +* 6133be4 Fix coding-style +* 252306a Update CI configuration +* 312ef14 Merge pull request #190 from philipsharp/response-body-summary +* 187ce72 Merge pull request #188 from miranovy/master +* edcb011 Rewind response body before generating summary for server errors +* a156f98 Distinguished name assert update + +## 15/01/2020 22:42 1.2.0 Release version 1.2.0 + +* d031223 Merge pull request #185 from miranovy/master +* 24b8575 Fix getIssuerCertificate return type +* df8f156 Merge pull request #183 from jderusse/split-request +* b86d2d6 Merge pull request #184 from miranovy/master +* ec1bbba fix the wrong return type +* 028470a fix the wrong return type +* 25a12e3 Split Sign and Request +* 937a2f7 Merge pull request #182 from pauladams8/master +* c592b85 added orderNotReady error type +* 0d09084 Merge pull request #180 from jderusse/fix-test +* f0e9422 Add php 7.3 and 7.4 +* 6b65f1a Merge pull request #164 from trustocean/feat-install-aliyun-cdn +* d060f0c style ci, no spacing near dot +* 222f184 重名时阿里云会报错 +51da67e Merge branch 'master' of https://github.com/acmephp/acmephp into * feat-install-aliyun-cdn +* 9203dec Merge pull request #163 from trustocean/feat-install-aliyun-waf +* ce4bb92 style ci +45037e1 Merge branch 'master' of https://github.com/acmephp/acmephp into * feat-install-aliyun-cdn +* dcd1ad7 fixbug +* bce7829 fixbug +* 3292b41 fix +* 2400302 remove wrong conf +* 0501e56 register in services +* 505d4c2 stash +* 0d522f7 Update InstallAliyunWafAction.php +* 91049e3 Remove try...catch wrapper +* e9fefc3 stash +* 2d88030 Merge pull request #161 from jderusse/deprecate-commands +* 723de09 style ci +* 1a17f6e fix style ci +* 40558ea close #17 +* 05a2f39 fix +* 8541342 statsh +* d865632 Merge pull request #162 from aik099/gandi-dns-solver-feat +* 83e47d3 Removed unused code +* 62d9b1b CS fixes +* 9eeb20a Adding Gandi.Net DNS solver class +* 8bae348 Deprecate commands in favor of run +* 9ef2916 Merge pull request #153 from elliotfehr/missed-memleak +* c1271c0 free openssl resource after reading +* b276743 Merge pull request #151 from elliotfehr/openssl-mem-leak +* 1effe3e Merge branch 'master' into openssl-mem-leak +* 58ee1e6 Merge pull request #152 from jderusse/fix-cs +* 2071f9d Fix CS +* bb55db6 free the key resource after reading 8c9d313 Build 1.1.1 PHAR -18/01/2019 15:17 1.1.1 Several bug fixes -952b1a6 Merge pull request #148 from rokclimb15/patch-1 -56df417 Correctly throw ChallengeTimedOutException -c7f523f Merge pull request #145 from jderusse/fix-exceptionx -034b6a2 Fix exception constructor -ff10617 Merge pull request #146 from jderusse/fix-deprec -9e754ad Fix 4.2 deprecations -10/11/2018 12:55 1.1.0 Add support for certificate revocation and ECDSA certificates -6933ffb Merge pull request #141 from jderusse/ecdsa -f7bac9e Fix undefined const OPENSSL_KEYTYPE_EC -5cf1a8d Add DH and DSA generators -c22cd9e Add support for ECDSA -3881f18 Merge pull request #143 from jderusse/update-phpunit -2cf3baf Use simple-phpunit to run tests -5194897 Merge pull request #142 from jderusse/fix-deps -4f13320 Add missing dependencies in composer.json files -da7c0e1 Merge pull request #139 from acmephp/revoke-certificate -39cf7fa Fix certificate revocation -27d4355 Update test to pass with Pebble implementation -94e2f20 Remove Certificate::__toString(), Command validation failure warning -> error -ef00e5f Add revocation reason and more helpful doc -c61019c Fix cs issues -7418bd9 Add api for certificate revocation -6d3888e Merge pull request #140 from acmephp/remove-scrutinizer -29258e6 Remove Scrutinizer -61df472 Merge pull request #138 from acmephp/remove-config-platform -af56ed7 Remove config.platform.php in Composer -e8029c2 Bump to dev -27/10/2018 12:07 1.0.1 Fix PHP version issue -6079833 Merge pull request #137 from acmephp/fix-php-version -6d6e2d2 Fix tests configuration -12ed5fc Fix PHP version in composer.json -958d497 Remove Gitter -a4effb8 Add link to core and ssl libraries in README -b17236e Bump to dev -14/10/2018 12:05 1.0.0 First stable release -1e4ba50 Merge pull request #135 from acmephp/prepare-release -13866eb Update README -9d773bb Remove beta messages -25e986e Prepare stable release, use only PrettyCI and fix CS -6267095 Merge pull request #134 from ScullWM/patch-1 -542ed28 Fix markdown error -c90e0a8 Merge pull request #132 from jderusse/optimize-route53 -c6b767a Optimize Route53 resolution -d4d2fa6 Merge pull request #131 from jderusse/catch-unresolvabled-nameserver -d57be04 Merge pull request #130 from alexmckinnon/csr-payload -1fd6427 Add common name to CSR payload -95f30c4 atch case where NameServer is not resolvable -421a4ab Merge pull request #128 from jderusse/update-dependencies -cdde24e Update dependencies -9b8ae4c Merge pull request #127 from jderusse/improve-libdns-fetching -d553270 Improve DNS checking -93bf725 Merge pull request #126 from jderusse/catch-libdns-exception-2 -c40f93b Catch exception on external calls -9bdd3ed Merge pull request #125 from jderusse/catch-libdns-exception -1930de5 Wrap external call in try/catch block -03b5532 Merge pull request #123 from jderusse/fix-status -c2b6b8d Allow "ready" status for orders with valid challenges -f039226 Merge pull request #119 from acmephp/auto-split -9ea70bc Automate split -32ccf3f Merge pull request #120 from jderusse/refactor-travis -2b5b296 Add comments -9b7ea2b Switch to travis pipeline -bbe4b9b Merge pull request #116 from jderusse/fix-sftp-config -db6c434 Merge pull request #115 from jderusse/fix-missing-lib -db391fc Add lib-xml used by SFTP adapter -922bd70 Fix typo in config -4a08ccc Merge pull request #113 from jderusse/fix-route53-domain-lookupx -bce7338 Fix Route53 zone search -c08891c Merge pull request #109 from jderusse/optimize-resolve -5dddf23 Optimize Route53 solvin -166dde3 Merge pull request #112 from kirtangajjar/fix-nginxproxy-wildcard -a848329 Merge pull request #111 from kirtangajjar/fix-nginxproxy-crt-generation -01f00c8 Merge pull request #107 from jderusse/feature-improve-error-messages -34bd808 Merge pull request #108 from jderusse/fix-payload -ca6531f Fix nginxproxy wildcard certificates -bac8140 Fixed in a bit better way -d081953 Fix nginxproxy crt generation -09ff221 Remove non linear index on array -26e672a Improve error messages -74dd66d Merge pull request #104 from benjilevens/laravel-5.5 -55d7b9b Merge pull request #106 from benjilevens/jose-json -2dd3303 Merge pull request #102 from jderusse/feature-skip-challenge -d4dcd9f Merge pull request #105 from benjilevens/request-certificate-calling-finalize-order -503c4df Use more appropriate Accept and Content-Type headers -475f359 Support multiple versions of swift mailer -4b9dac7 Make requestCertificate call finalizeOrder with required parameters -005081d Bump Swiftmailer version to prevent composer conflicts when installing into a Laravel 5.5 project -82e5e5b Skip challenge when no renewal -fd54a08 Merge pull request #101 from jderusse/feature-multiple-challenges -dd88adc Optimize challenge solving by solving several challenges at once -0e618e3 Merge pull request #100 from jderusse/feature-file-solver -898c939 Use service locator -04c9814 Add a filesystem solver (to upload http challenge) -9818d39 Merge pull request #98 from jderusse/fix-tree -208913f Refactor file tree -4966d60 Merge pull request #96 from jderusse/fix-combined-public -1671a5b Merge pull request #97 from jderusse/fix-status-expired -e327321 Add an option to hide/show expired certificates -51ebab0 Move combined certificate in private folder -bd1478e Merge pull request #95 from jderusse/fix-ci -a08cb2e Fix CS -1d590bb Switch from styleCi to travis -477929d Merge pull request #94 from jderusse/fix-debug -75552fd Remove debug -c4b8c66 Merge pull request #87 from jderusse/feature/run -38e49cb Add a run command -15c1809 Merge pull request #93 from jderusse/feature/docker -9907669 Add a dockerfile -a7b8581 Merge pull request #92 from jderusse/v2 -1550972 Implement v2 protocol -6d15380 Implement ELB installation -3c8b06a Add Route53 solver -f306733 Bump to dev -21/01/2018 18:31 1.0.0-beta5 Fix deprecations and allow setting KeyPair from Client object + +## 18/01/2019 15:17 1.1.1 Several bug fixes + +* 952b1a6 Merge pull request #148 from rokclimb15/patch-1 +* 56df417 Correctly throw ChallengeTimedOutException +* c7f523f Merge pull request #145 from jderusse/fix-exceptionx +* 034b6a2 Fix exception constructor +* ff10617 Merge pull request #146 from jderusse/fix-deprec +* 9e754ad Fix 4.2 deprecations +* 10/11/2018 12:55 1.1.0 Add support for certificate revocation and ECDSA certificates +* 6933ffb Merge pull request #141 from jderusse/ecdsa +* f7bac9e Fix undefined const OPENSSL_KEYTYPE_EC +* 5cf1a8d Add DH and DSA generators +* c22cd9e Add support for ECDSA +* 3881f18 Merge pull request #143 from jderusse/update-phpunit +* 2cf3baf Use simple-phpunit to run tests +* 5194897 Merge pull request #142 from jderusse/fix-deps +* 4f13320 Add missing dependencies in composer.json files +* da7c0e1 Merge pull request #139 from acmephp/revoke-certificate +* 39cf7fa Fix certificate revocation +* 27d4355 Update test to pass with Pebble implementation +* 94e2f20 Remove Certificate::__toString(), Command validation failure warning -> error +* ef00e5f Add revocation reason and more helpful doc +* c61019c Fix cs issues +* 7418bd9 Add api for certificate revocation +* 6d3888e Merge pull request #140 from acmephp/remove-scrutinizer +* 29258e6 Remove Scrutinizer +* 61df472 Merge pull request #138 from acmephp/remove-config-platform +* af56ed7 Remove config.platform.php in Composer +* e8029c2 Bump to dev + +## 27/10/2018 12:07 1.0.1 Fix PHP version issue + +* 6079833 Merge pull request #137 from acmephp/fix-php-version +* 6d6e2d2 Fix tests configuration +* 12ed5fc Fix PHP version in composer.json +* 958d497 Remove Gitter +* a4effb8 Add link to core and ssl libraries in README +* b17236e Bump to dev + +## 14/10/2018 12:05 1.0.0 First stable release + +* 1e4ba50 Merge pull request #135 from acmephp/prepare-release +* 13866eb Update README +* 9d773bb Remove beta messages +* 25e986e Prepare stable release, use only PrettyCI and fix CS +* 6267095 Merge pull request #134 from ScullWM/patch-1 +* 542ed28 Fix markdown error +* c90e0a8 Merge pull request #132 from jderusse/optimize-route53 +* c6b767a Optimize Route53 resolution +* d4d2fa6 Merge pull request #131 from jderusse/catch-unresolvabled-nameserver +* d57be04 Merge pull request #130 from alexmckinnon/csr-payload +* 1fd6427 Add common name to CSR payload +* 95f30c4 atch case where NameServer is not resolvable +* 421a4ab Merge pull request #128 from jderusse/update-dependencies +* cdde24e Update dependencies +* 9b8ae4c Merge pull request #127 from jderusse/improve-libdns-fetching +* d553270 Improve DNS checking +* 93bf725 Merge pull request #126 from jderusse/catch-libdns-exception-2 +* c40f93b Catch exception on external calls +* 9bdd3ed Merge pull request #125 from jderusse/catch-libdns-exception +* 1930de5 Wrap external call in try/catch block +* 03b5532 Merge pull request #123 from jderusse/fix-status +* c2b6b8d Allow "ready" status for orders with valid challenges +* f039226 Merge pull request #119 from acmephp/auto-split +* 9ea70bc Automate split +* 32ccf3f Merge pull request #120 from jderusse/refactor-travis +* 2b5b296 Add comments +* 9b7ea2b Switch to travis pipeline +* bbe4b9b Merge pull request #116 from jderusse/fix-sftp-config +* db6c434 Merge pull request #115 from jderusse/fix-missing-lib +* db391fc Add lib-xml used by SFTP adapter +* 922bd70 Fix typo in config +* 4a08ccc Merge pull request #113 from jderusse/fix-route53-domain-lookupx +* bce7338 Fix Route53 zone search +* c08891c Merge pull request #109 from jderusse/optimize-resolve +* 5dddf23 Optimize Route53 solvin +* 166dde3 Merge pull request #112 from kirtangajjar/fix-nginxproxy-wildcard +* a848329 Merge pull request #111 from kirtangajjar/fix-nginxproxy-crt-generation +* 01f00c8 Merge pull request #107 from jderusse/feature-improve-error-messages +* 34bd808 Merge pull request #108 from jderusse/fix-payload +* ca6531f Fix nginxproxy wildcard certificates +* bac8140 Fixed in a bit better way +* d081953 Fix nginxproxy crt generation +* 09ff221 Remove non linear index on array +* 26e672a Improve error messages +* 74dd66d Merge pull request #104 from benjilevens/laravel-5.5 +* 55d7b9b Merge pull request #106 from benjilevens/jose-json +* 2dd3303 Merge pull request #102 from jderusse/feature-skip-challenge +* d4dcd9f Merge pull request #105 from benjilevens/request-certificate-calling-finalize-order +* 503c4df Use more appropriate Accept and Content-Type headers +* 475f359 Support multiple versions of swift mailer +* 4b9dac7 Make requestCertificate call finalizeOrder with required parameters +005081d Bump Swiftmailer version to prevent composer conflicts when installing into a * Laravel 5.5 project +* 82e5e5b Skip challenge when no renewal +* fd54a08 Merge pull request #101 from jderusse/feature-multiple-challenges +* dd88adc Optimize challenge solving by solving several challenges at once +* 0e618e3 Merge pull request #100 from jderusse/feature-file-solver +* 898c939 Use service locator +* 04c9814 Add a filesystem solver (to upload http challenge) +* 9818d39 Merge pull request #98 from jderusse/fix-tree +* 208913f Refactor file tree +* 4966d60 Merge pull request #96 from jderusse/fix-combined-public +* 1671a5b Merge pull request #97 from jderusse/fix-status-expired +* e327321 Add an option to hide/show expired certificates +* 51ebab0 Move combined certificate in private folder +* bd1478e Merge pull request #95 from jderusse/fix-ci +* a08cb2e Fix CS +* 1d590bb Switch from styleCi to travis +* 477929d Merge pull request #94 from jderusse/fix-debug +* 75552fd Remove debug +* c4b8c66 Merge pull request #87 from jderusse/feature/run +* 38e49cb Add a run command +* 15c1809 Merge pull request #93 from jderusse/feature/docker +* 9907669 Add a dockerfile +* a7b8581 Merge pull request #92 from jderusse/v2 +* 1550972 Implement v2 protocol +* 6d15380 Implement ELB installation +* 3c8b06a Add Route53 solver +* f306733 Bump to dev + +## 21/01/2018 18:31 1.0.0-beta5 Fix deprecations and allow setting KeyPair from Client object + 466e009 Release of new version 1.0.0-beta5 0a94b15 Fix mailer handler 8fde026 Allow setting KeyPair from Client object (#72) @@ -167,14 +295,17 @@ e2ac2bf Fix json_decode error handling in SecureHttpClient and ServerErrorHandle 4c7d10d Fix deprecations and improve tests f559a93 [doc] Fix typo on README 0b4ea8b Bump to dev -21/01/2018 18:23 1.0.0-beta5 Fix deprecations and allow setting KeyPair from Client object -0a94b15 Fix mailer handler -8fde026 Allow setting KeyPair from Client object (#72) -e2ac2bf Fix json_decode error handling in SecureHttpClient and ServerErrorHandler -4c7d10d Fix deprecations and improve tests -f559a93 [doc] Fix typo on README -0b4ea8b Bump to dev -* 1.0.0-beta4 (2017-01-29) + +## 21/01/2018 18:23 1.0.0-beta5 Fix deprecations and allow setting KeyPair from Client object + +* 0a94b15 Fix mailer handler +* 8fde026 Allow setting KeyPair from Client object (#72) +* e2ac2bf Fix json_decode error handling in SecureHttpClient and ServerErrorHandler +* 4c7d10d Fix deprecations and improve tests +* f559a93 [doc] Fix typo on README +* 0b4ea8b Bump to dev + +## 1.0.0-beta4 (2017-01-29) * 0b18a86 Redone nameing of ::getResource with new tests (#61) * 78d0e2d Accept empty issuer CN field (#59) @@ -192,7 +323,7 @@ f559a93 [doc] Fix typo on README * a7f194a Fix vendor/bin/acme autoload path * 5e4a887 Bump to dev -* 1.0.0-beta3 (2016-12-09) +## 1.0.0-beta3 (2016-12-09) * daa6d1c Merge pull request #40 from acmephp/fix-39 * 0b8629e Remove RECOVER_REGISTRATION obsolete unused resource @@ -204,7 +335,7 @@ f559a93 [doc] Fix typo on README * 794f8d5 Introduce CLI logger to handle different verbosities properly * e055695 Bump to dev -* 1.0.0-beta2 (2016-10-19) +## 1.0.0-beta2 (2016-10-19) * ed2cc9e Update main and components README files * 1adff0d Improve some commands descriptions @@ -226,7 +357,7 @@ f559a93 [doc] Fix typo on README * 1d1bf00 Rename challenger into SOlver * e1289df Allow custom challenger extension -* 1.0.0-beta1 (2016-09-24) +## 1.0.0-beta1 (2016-09-24) * f1585a4 Fix type in README * 54d68c3 Merge pull request #24 from jderusse/multi-domains @@ -237,12 +368,12 @@ f559a93 [doc] Fix typo on README * f7de82d Automatically agreed with agrement (#26) * 7accf30 Fix tests (#27) -* 1.0.0-alpha10 (2016-08-16) +## 1.0.0-alpha10 (2016-08-16) * 3bfa96c Update RegisterCommand.php (#21) * d3d779f Bump version -* 1.0.0-alpha9 (2016-07-27) +## 1.0.0-alpha9 (2016-07-27) * 3e89b38 Remove unsupported actions from the dist file for the moment * 07857e6 Add PHP 7.1 in Travis and improve CI configuration (#19) @@ -250,4 +381,4 @@ f559a93 [doc] Fix typo on README * adf4dc8 Update version as DEV * f61df6f Fix Guzzle URI test (#17) * 846bbce Fix assertions messages - * 113b2d8 Fix 404 on documentation link (#15) \ No newline at end of file + * 113b2d8 Fix 404 on documentation link (#15) diff --git a/Dockerfile b/Dockerfile index b67283f0..a666b269 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,76 +1,31 @@ -FROM alpine:3.8 AS builder - -COPY --from=composer:latest /usr/bin/composer /usr/bin/composer - -WORKDIR /srv - -# Composer dependencies -RUN apk add --no-cache \ - php7 \ - php7-phar \ - php7-json \ - php7-iconv \ - php7-mbstring \ - php7-curl \ - php7-ctype \ - php7-opcache \ - php7-sockets \ - php7-openssl - -RUN composer global require "hirak/prestissimo" "jderusse/composer-warmup" - -RUN echo "opcache.enable_cli=1" > /etc/php7/conf.d/opcache.ini \ - && echo "opcache.file_cache='/tmp/opcache'" >> /etc/php7/conf.d/opcache.ini \ - && echo "opcache.file_update_protection=0" >> /etc/php7/conf.d/opcache.ini \ - && mkdir /tmp/opcache - -COPY composer.json /srv/ - -# App dependencies +FROM alpine:edge +ADD acmephp.phar /srv/bin/acme RUN apk add --no-cache \ - php7-simplexml \ - php7-dom \ - php7-tokenizer - -RUN composer install --no-dev --no-scripts --no-suggest --optimize-autoloader \ - && composer require "daverandom/libdns:^2.0.1" --no-scripts --no-suggest --optimize-autoloader - -COPY ./src /srv/src -COPY ./res /srv/res -COPY ./bin /srv/bin - -RUN composer warmup-opcode -- /srv - -# ============================= - -FROM alpine:3.8 + php83 \ + php83-opcache \ + php83-apcu \ + php83-openssl \ + php83-dom \ + php83-mbstring \ + php83-json \ + php83-ctype \ + php83-posix \ + php83-simplexml \ + php83-xmlwriter \ + php83-xml \ + php83-phar \ + php83-curl \ + php83-fileinfo \ + php83-sodium \ + ca-certificates WORKDIR /srv - -# PHP -RUN apk add --no-cache \ - php7 \ - php7-opcache \ - php7-apcu \ - php7-openssl \ - php7-dom \ - php7-mbstring \ - php7-json \ - php7-ctype \ - php7-posix \ - php7-simplexml \ - php7-xmlwriter \ - php7-xml \ - ca-certificates - -RUN echo "date.timezone = UTC" > /etc/php7/conf.d/symfony.ini \ - && echo "opcache.enable_cli=1" > /etc/php7/conf.d/opcache.ini \ - && echo "opcache.file_cache='/tmp/opcache'" >> /etc/php7/conf.d/opcache.ini \ - && echo "opcache.file_update_protection=0" >> /etc/php7/conf.d/opcache.ini \ +RUN chmod +x /srv/bin/acme +RUN echo "date.timezone = UTC" > /etc/php83/conf.d/symfony.ini \ + && echo "opcache.enable_cli=1" > /etc/php83/conf.d/opcache.ini \ + && echo "opcache.file_cache='/tmp/opcache'" >> /etc/php83/conf.d/opcache.ini \ + && echo "opcache.file_update_protection=0" >> /etc/php83/conf.d/opcache.ini \ && mkdir /tmp/opcache ENTRYPOINT ["/srv/bin/acme"] CMD ["list"] - -COPY --from=builder /tmp/opcache /tmp/opcache -COPY --from=builder /srv /srv diff --git a/README.md b/README.md index e0a8b7ae..9247c2eb 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,10 @@ Acme PHP ======== -[![Build Status](https://img.shields.io/travis/acmephp/acmephp/master.svg?style=flat-square)](https://travis-ci.org/acmephp/acmephp) -[![StyleCI](https://styleci.io/repos/59910490/shield)](https://styleci.io/repos/59910490) +[![Build Status](https://img.shields.io/github/actions/workflow/status/acmephp/acmephp/test-build.yaml?branch=master&style=flat-square)](https://github.com/acmephp/acmephp/actions/workflows/test-build.yaml?query=branch%3Amaster) [![Packagist Version](https://img.shields.io/packagist/v/acmephp/acmephp.svg?style=flat-square)](https://packagist.org/packages/acmephp/acmephp) [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) -[![SymfonyInsight](https://insight.symfony.com/projects/4eb121bf-9f9d-4d16-813b-f98f07003eaf/big.svg)](https://insight.symfony.com/projects/4eb121bf-9f9d-4d16-813b-f98f07003eaf) - Acme PHP is a simple yet very extensible CLI client for Let's Encrypt that will help you get and renew free HTTPS certificates. @@ -18,17 +15,17 @@ able to deeply integrate the management of your certificates directly in your ap by these features, have a look at the [acmephp/core](https://github.com/acmephp/core) and [acmephp/ssl](https://github.com/acmephp/ssl) libraries. -> If you want to chat with us or have questions, ping -> @tgalopin or @jderusse on the [Symfony Slack](https://symfony.com/support)! +> Acme PHP is now maintained by https://zerossl.com +> +> Other companies are welcome to contribute to the maintenance of the library. Thanks to: +> - https://redirection.io/ ## Why should I use Acme PHP when I have an official client? Acme PHP provides several major improvements over the default clients: - Acme PHP comes by nature as a single binary file: a single download and you are ready to start working ; -- Acme PHP is based on a configuration file (`~/.acmephp/acmephp.conf`) instead command line arguments. +- Acme PHP is based on a configuration file instead command line arguments. Thus, the configuration is much more expressive and the same setup is used at every renewal ; -- Acme PHP can monitor your CRONs and can send you alerts in many differents places: - E-mail, Slack, HipChat, Flowdock, Fleep (thanks to [Monolog](https://github.com/Seldaek/monolog)!) - Acme PHP is very extensible it to create the certificate files structure you need for your webserver. It brings several default formatters to create classical file structures (nginx, nginx-proxy, haproxy, etc.) but you can very easily create your own if you need to ; @@ -45,14 +42,6 @@ Acme PHP follows a strict BC policy by sticking carefully to [semantic versionin your scripts, your CRON tasks and your code will keep working properly even when you update Acme PHP (either the CLI tool or the library), as long as you keep the same major version (1.X.X, 2.X.X, etc.). -In addition of semantic versioning of stable versions for the CLI and the library, Acme PHP also follows -certain rules **for the CLI only**: -- an alpha release can break BC with previous alpha releases of the same version - (1.1.0-alpha2 can break BC with features introduced by 1.1.0-alpha1 but can't break BC with 1.0.0 features). -- a beta release cannot break BC with previous beta releases - (1.1.0-beta4 have to be BC with 1.1.0-beta3, 1.1.0-beta2, 1.1.0-beta1 and 1.0.0). New features can be added in beta - as long as they don't break BC. - ## Launch the Test suite The Acme PHP test suite uses the Docker Boulder image to create an ACME server. diff --git a/bin/acme b/bin/acme index b9c0dd24..d164e5c9 100755 --- a/bin/acme +++ b/bin/acme @@ -10,8 +10,8 @@ * file that was distributed with this source code. */ -if (version_compare('5.5.0', PHP_VERSION, '>')) { - echo 'This version of Acme PHP requires PHP 5.5.0.'.PHP_EOL; +if (version_compare('8.1.0', PHP_VERSION, '>')) { + echo 'This version of Acme PHP requires PHP 8.1.0.'.PHP_EOL; exit; } diff --git a/box.json.dist b/box.json.dist index c3d40d75..f477b50c 100644 --- a/box.json.dist +++ b/box.json.dist @@ -1,9 +1,9 @@ { - "directories": [ "src", "res" ], + "directories": [ "src" ], "files": [ "LICENSE" ], "finder": [ { - "name": "*.php", + "name": "{\\.(php|bash|fish|zsh)}", "exclude": [ "Tester", "Tests", @@ -16,12 +16,8 @@ "in": "vendor" } ], - "compactors": [ "Herrera\\Box\\Compactor\\Php" ], + "git": "git", "compression": "GZ", "git-version": "package_version", - "main": "bin/acme", - "output": "build/acmephp.phar", - "chmod": "0755", - "stub": true, - "key": "../private.key" + "output": "build/acmephp.phar" } diff --git a/composer.json b/composer.json index 0f79c2d4..a6736502 100644 --- a/composer.json +++ b/composer.json @@ -4,6 +4,9 @@ "type": "project", "license": "MIT", "homepage": "https://github.com/acmephp/acmephp", + "bin": [ + "bin/acme" + ], "keywords": [ "acme", "acmephp", @@ -31,44 +34,41 @@ } ], "require": { - "php": ">=5.5.9", + "php": ">=8.3", + "ext-filter": "*", "ext-hash": "*", "ext-json": "*", - "ext-filter": "*", "ext-mbstring": "*", "ext-openssl": "*", "lib-openssl": ">=0.9.8", - "aws/aws-sdk-php": "^3.38", - "guzzlehttp/guzzle": "^6.0", - "guzzlehttp/psr7": "^1.0", - "league/flysystem": "^1.0.19", - "league/flysystem-memory": "^1.0", - "league/flysystem-sftp": "^1.0.7", - "monolog/monolog": "^1.19", - "padraic/phar-updater": "^1.0", - "psr/container": "^1.0", - "psr/http-message": "^1.0", - "psr/log": "^1.0", - "swiftmailer/swiftmailer": "^5.4|^6.0", - "symfony/config": "^3.0|^4.0", - "symfony/console": "^3.0|^4.0", - "symfony/dependency-injection": "^3.3|^4.0", - "symfony/filesystem": "^3.0|^4.0", - "symfony/serializer": "^3.0|^4.0", - "symfony/yaml": "^3.0|^4.0", - "webmozart/assert": "^1.0", - "webmozart/path-util": "^2.3", "alibabacloud/cdn": "^1.7", - "alibabacloud/wafopenapi": "^1.7" - }, - "suggest": { - "daverandom/libdns": "^2.0" + "alibabacloud/wafopenapi": "^1.7", + "aws/aws-sdk-php": "^3.38", + "consolidation/self-update": "^2.2 || ^3", + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^2.4.5", + "lcobucci/jwt": "^5.3", + "league/flysystem": "^3.10", + "league/flysystem-memory": "^3.10", + "league/flysystem-sftp-v3": "^3.10", + "monolog/monolog": "^3", + "psr/container": "^1 || ^2", + "psr/http-message": "^1 || ^2", + "psr/log": "^2 || ^3", + "symfony/config": "^5.4.12 || ^6.4 || ^7.1", + "symfony/console": "^5.4.12 || ^6.4 || ^7.1", + "symfony/dependency-injection": "^5.4.12 || ^6.4 || ^7.1", + "symfony/filesystem": "^5.4.12 || ^6.4 || ^7.1", + "symfony/serializer": "^5.4.12 || ^6.4 || ^7.1", + "symfony/yaml": "^5.4.12 || ^6.4 || ^7.1", + "webmozart/assert": "^1.0" }, "require-dev": { - "phpspec/prophecy": "^1.9", - "symfony/finder": "^3.4|^4.0", - "symfony/phpunit-bridge": "^5.0", - "symfony/var-dumper": "^3.4|^4.0" + "phpstan/phpstan": "^1.11.11", + "symfony/finder": "^5.4.12 || ^6.4 || ^7.1", + "symfony/phpunit-bridge": "^5.4.12 || ^6.4 || ^7.1", + "symfony/property-access": "^5.4.12 || ^6.4 || ^7.1", + "symfony/var-dumper": "^5.4.12 || ^6.4 || ^7.1" }, "autoload": { "psr-4": { @@ -80,12 +80,7 @@ "Tests\\AcmePhp\\": "tests/" } }, - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "bin": [ - "bin/acme" - ] + "config": { + "sort-packages": true + } } diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 00000000..24d4273b --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,421 @@ +parameters: + ignoreErrors: + - + message: "#^Call to an undefined method AlibabaCloud\\\\WafOpenapi\\\\V20161111\\\\UpgradeInstance\\:\\:withCert\\(\\)\\.$#" + count: 1 + path: src/Cli/Action/InstallAliyunWafAction.php + + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Console\\\\Application\\:\\:getStorageDirectory\\(\\)\\.$#" + count: 1 + path: src/Cli/Command/AbstractCommand.php + + - + message: "#^Call to an undefined method object\\:\\:createSecureHttpClient\\(\\)\\.$#" + count: 1 + path: src/Cli/Command/AbstractCommand.php + + - + message: "#^Method AcmePhp\\\\Cli\\\\Command\\\\AbstractCommand\\:\\:getCliLogger\\(\\) should return Psr\\\\Log\\\\LoggerInterface but returns object\\.$#" + count: 1 + path: src/Cli/Command/AbstractCommand.php + + - + message: "#^Method AcmePhp\\\\Cli\\\\Command\\\\AbstractCommand\\:\\:getRepository\\(\\) should return AcmePhp\\\\Cli\\\\Repository\\\\RepositoryInterface but returns object\\.$#" + count: 1 + path: src/Cli/Command/AbstractCommand.php + + - + message: "#^Access to an undefined property object\\:\\:\\$eab_hmac_key\\.$#" + count: 2 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Access to an undefined property object\\:\\:\\$eab_kid\\.$#" + count: 2 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Call to an undefined method object\\:\\:generateKeyPair\\(\\)\\.$#" + count: 2 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Call to an undefined method object\\:\\:get\\(\\)\\.$#" + count: 1 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Call to an undefined method object\\:\\:parse\\(\\)\\.$#" + count: 2 + path: src/Cli/Command/RunCommand.php + + - + message: "#^PHPDoc tag @var for variable \\$solverLocator contains generic class Symfony\\\\Component\\\\DependencyInjection\\\\ServiceLocator but does not specify its types\\: T$#" + count: 1 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Parameter \\#1 \\$input of static method Symfony\\\\Component\\\\Yaml\\\\Yaml\\:\\:parse\\(\\) expects string, string\\|false given\\.$#" + count: 1 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Property AcmePhp\\\\Cli\\\\Command\\\\RunCommand\\:\\:\\$config has no type specified\\.$#" + count: 1 + path: src/Cli/Command/RunCommand.php + + - + message: "#^Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder\\:\\:root\\(\\)\\.$#" + count: 5 + path: src/Cli/Configuration/DomainConfiguration.php + + - + message: "#^Class Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\TreeBuilder constructor invoked with 0 parameters, 1\\-3 required\\.$#" + count: 4 + path: src/Cli/Configuration/DomainConfiguration.php + + - + message: "#^Parameter \\#1 \\$string of function sha1 expects string, string\\|false given\\.$#" + count: 1 + path: src/Cli/Repository/Repository.php + + - + message: "#^Method AcmePhp\\\\Core\\\\AcmeClient\\:\\:registerAccount\\(\\) should return array but returns array\\|string\\.$#" + count: 1 + path: src/Core/AcmeClient.php + + - + message: "#^Negated boolean expression is always false\\.$#" + count: 2 + path: src/Core/AcmeClient.php + + - + message: "#^Offset 'challenges' does not exist on array\\|string\\.$#" + count: 2 + path: src/Core/AcmeClient.php + + - + message: "#^Offset 'identifier' does not exist on array\\|string\\.$#" + count: 2 + path: src/Core/AcmeClient.php + + - + message: "#^Offset 'status' does not exist on array\\|string\\.$#" + count: 2 + path: src/Core/AcmeClient.php + + - + message: "#^Parameter \\#1 \\$certificate of function openssl_x509_export expects OpenSSLCertificate\\|string, OpenSSLCertificate\\|false given\\.$#" + count: 1 + path: src/Core/AcmeClient.php + + - + message: "#^Parameter \\#1 \\$serverResources of class AcmePhp\\\\Core\\\\Protocol\\\\ResourcesDirectory constructor expects array, array\\|string given\\.$#" + count: 1 + path: src/Core/AcmeClient.php + + - + message: "#^Parameter \\#1 \\$string of function trim expects string, array\\\\|string given\\.$#" + count: 1 + path: src/Core/AcmeClient.php + + - + message: "#^Parameter \\#2 \\$certificate of method AcmePhp\\\\Core\\\\AcmeClient\\:\\:createCertificateResponse\\(\\) expects string, array\\|string given\\.$#" + count: 1 + path: src/Core/AcmeClient.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\GandiSolver\\:\\:\\$cacheZones is unused\\.$#" + count: 1 + path: src/Core/Challenge/Dns/GandiSolver.php + + - + message: "#^Access to constant NS on an unknown class LibDNS\\\\Records\\\\ResourceTypes\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Access to constant QUERY on an unknown class LibDNS\\\\Messages\\\\MessageTypes\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Access to constant TXT on an unknown class LibDNS\\\\Records\\\\ResourceTypes\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Call to method create\\(\\) on an unknown class LibDNS\\\\Decoder\\\\DecoderFactory\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Call to method create\\(\\) on an unknown class LibDNS\\\\Encoder\\\\EncoderFactory\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Call to method create\\(\\) on an unknown class LibDNS\\\\Messages\\\\MessageFactory\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Call to method create\\(\\) on an unknown class LibDNS\\\\Records\\\\QuestionFactory\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Call to method decode\\(\\) on an unknown class LibDNS\\\\Decoder\\\\Decoder\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Call to method encode\\(\\) on an unknown class LibDNS\\\\Encoder\\\\Encoder\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Instantiated class LibDNS\\\\Decoder\\\\DecoderFactory not found\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Instantiated class LibDNS\\\\Encoder\\\\EncoderFactory not found\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Instantiated class LibDNS\\\\Messages\\\\MessageFactory not found\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Instantiated class LibDNS\\\\Records\\\\QuestionFactory not found\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\#1 \\$json of function json_decode expects string, int\\|string given\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\#1 \\$read of function stream_select expects TRead of array\\\\|null, array\\ given\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\#1 \\$socket of function stream_socket_sendto expects resource, resource\\|false given\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\#1 \\$stream of function fread expects resource, resource\\|false given\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\$decoder of method AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:__construct\\(\\) has invalid type LibDNS\\\\Decoder\\\\Decoder\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\$encoder of method AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:__construct\\(\\) has invalid type LibDNS\\\\Encoder\\\\Encoder\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\$messageFactory of method AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:__construct\\(\\) has invalid type LibDNS\\\\Messages\\\\MessageFactory\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\$questionFactory of method AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:__construct\\(\\) has invalid type LibDNS\\\\Records\\\\QuestionFactory\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:\\$decoder has unknown class LibDNS\\\\Decoder\\\\Decoder as its type\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:\\$encoder has unknown class LibDNS\\\\Encoder\\\\Encoder as its type\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:\\$messageFactory has unknown class LibDNS\\\\Messages\\\\MessageFactory as its type\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Challenge\\\\Dns\\\\LibDnsResolver\\:\\:\\$questionFactory has unknown class LibDNS\\\\Records\\\\QuestionFactory as its type\\.$#" + count: 1 + path: src/Core/Challenge/Dns/LibDnsResolver.php + + - + message: "#^Parameter \\#1 \\$string of function addslashes expects string, int\\|string\\|false given\\.$#" + count: 1 + path: src/Core/Challenge/Dns/Route53Solver.php + + - + message: "#^Argument of an invalid type array\\\\|false supplied for foreach, only iterables are supported\\.$#" + count: 1 + path: src/Core/Challenge/Dns/SimpleDnsResolver.php + + - + message: "#^Constructor of class AcmePhp\\\\Core\\\\Exception\\\\AcmeCoreServerException has an unused parameter \\$request\\.$#" + count: 1 + path: src/Core/Exception/AcmeCoreServerException.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Exception\\\\Protocol\\\\ChallengeFailedException\\:\\:\\$response has no type specified\\.$#" + count: 1 + path: src/Core/Exception/Protocol/ChallengeFailedException.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Exception\\\\Protocol\\\\ChallengeTimedOutException\\:\\:\\$response has no type specified\\.$#" + count: 1 + path: src/Core/Exception/Protocol/ChallengeTimedOutException.php + + - + message: "#^Instantiated class League\\\\Flysystem\\\\Adapter\\\\Ftp not found\\.$#" + count: 1 + path: src/Core/Filesystem/Adapter/FlysystemFtpFactory.php + + - + message: "#^Parameter \\#1 \\$adapter of class League\\\\Flysystem\\\\Filesystem constructor expects League\\\\Flysystem\\\\FilesystemAdapter, League\\\\Flysystem\\\\Adapter\\\\Ftp given\\.$#" + count: 1 + path: src/Core/Filesystem/Adapter/FlysystemFtpFactory.php + + - + message: "#^Instantiated class League\\\\Flysystem\\\\Adapter\\\\Local not found\\.$#" + count: 1 + path: src/Core/Filesystem/Adapter/FlysystemLocalFactory.php + + - + message: "#^Parameter \\#1 \\$adapter of class League\\\\Flysystem\\\\Filesystem constructor expects League\\\\Flysystem\\\\FilesystemAdapter, League\\\\Flysystem\\\\Adapter\\\\Local given\\.$#" + count: 1 + path: src/Core/Filesystem/Adapter/FlysystemLocalFactory.php + + - + message: "#^Left side of && is always true\\.$#" + count: 1 + path: src/Core/Http/SecureHttpClient.php + + - + message: "#^Parameter \\#1 \\$contents of static method Lcobucci\\\\JWT\\\\Signer\\\\Key\\\\InMemory\\:\\:plainText\\(\\) expects non\\-empty\\-string, string given\\.$#" + count: 1 + path: src/Core/Http/SecureHttpClient.php + + - + message: "#^Parameter \\#1 \\$input of method AcmePhp\\\\Core\\\\Http\\\\Base64SafeEncoder\\:\\:encode\\(\\) expects string, string\\|false given\\.$#" + count: 4 + path: src/Core/Http/SecureHttpClient.php + + - + message: "#^Parameter \\#2 \\$data of function hash expects string, string\\|false given\\.$#" + count: 1 + path: src/Core/Http/SecureHttpClient.php + + - + message: "#^Parameter \\#2 \\$key of method Lcobucci\\\\JWT\\\\Signer\\\\Hmac\\:\\:sign\\(\\) expects Lcobucci\\\\JWT\\\\Signer\\\\Key, Lcobucci\\\\JWT\\\\Signer\\\\Key\\\\InMemory\\|string given\\.$#" + count: 1 + path: src/Core/Http/SecureHttpClient.php + + - + message: "#^Parameter \\#2 \\$payload of method AcmePhp\\\\Core\\\\Http\\\\SecureHttpClient\\:\\:signPayload\\(\\) expects array\\|null, array\\|string\\|null given\\.$#" + count: 2 + path: src/Core/Http/SecureHttpClient.php + + - + message: "#^Method AcmePhp\\\\Core\\\\Http\\\\ServerErrorHandler\\:\\:createAcmeExceptionForResponse\\(\\) should return AcmePhp\\\\Core\\\\Exception\\\\AcmeCoreServerException but returns object\\.$#" + count: 1 + path: src/Core/Http/ServerErrorHandler.php + + - + message: "#^Property AcmePhp\\\\Core\\\\Http\\\\ServerErrorHandler\\:\\:\\$exceptions has no type specified\\.$#" + count: 1 + path: src/Core/Http/ServerErrorHandler.php + + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 1 + path: src/Core/Protocol/RevocationReason.php + + - + message: "#^Parameter \\#3 \\$depth of function json_decode expects int\\<1, max\\>, int given\\.$#" + count: 1 + path: src/Core/Util/JsonDecoder.php + + - + message: "#^Cannot access offset 'key' on array\\|false\\.$#" + count: 1 + path: src/Ssl/Certificate.php + + - + message: "#^Method AcmePhp\\\\Ssl\\\\Certificate\\:\\:getPublicKeyResource\\(\\) should return resource but returns OpenSSLAsymmetricKey\\.$#" + count: 1 + path: src/Ssl/Certificate.php + + - + message: "#^Parameter \\#1 \\$key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, resource given\\.$#" + count: 1 + path: src/Ssl/Certificate.php + + - + message: "#^Property AcmePhp\\\\Ssl\\\\Generator\\\\ChainPrivateKeyGenerator\\:\\:\\$generators \\(array\\\\) does not accept iterable\\\\.$#" + count: 1 + path: src/Ssl/Generator/ChainPrivateKeyGenerator.php + + - + message: "#^Parameter \\#2 \\$values of static method Webmozart\\\\Assert\\\\Assert\\:\\:oneOf\\(\\) expects array, array\\\\|false given\\.$#" + count: 1 + path: src/Ssl/Generator/EcKey/EcKeyOption.php + + - + message: "#^Parameter \\#1 \\$key of function openssl_free_key expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey\\|resource given\\.$#" + count: 1 + path: src/Ssl/Parser/KeyParser.php + + - + message: "#^Parameter \\#1 \\$key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey\\|resource given\\.$#" + count: 1 + path: src/Ssl/Parser/KeyParser.php + + - + message: "#^Parameter \\#1 \\$key of function openssl_free_key expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey\\|resource given\\.$#" + count: 1 + path: src/Ssl/PrivateKey.php + + - + message: "#^Parameter \\#1 \\$key of function openssl_pkey_get_details expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey\\|resource given\\.$#" + count: 1 + path: src/Ssl/PrivateKey.php + + - + message: "#^Cannot access offset 1 on array\\|false\\.$#" + count: 1 + path: src/Ssl/Signer/DataSigner.php + + - + message: "#^Parameter \\#1 \\$key of function openssl_free_key expects OpenSSLAsymmetricKey, OpenSSLAsymmetricKey\\|resource given\\.$#" + count: 1 + path: src/Ssl/Signer/DataSigner.php + + - + message: "#^Parameter \\#2 \\$start of function mb_substr expects int, float\\|int given\\.$#" + count: 1 + path: src/Ssl/Signer/DataSigner.php + + - + message: "#^Parameter \\#3 \\$length of function mb_substr expects int\\|null, float\\|int given\\.$#" + count: 2 + path: src/Ssl/Signer/DataSigner.php + + - + message: "#^Parameter \\#3 \\$private_key of function openssl_sign expects array\\|OpenSSLAsymmetricKey\\|OpenSSLCertificate\\|string, OpenSSLAsymmetricKey\\|resource given\\.$#" + count: 1 + path: src/Ssl/Signer/DataSigner.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 00000000..17850572 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,19 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 7 + excludePaths: + - src/Core/vendor + paths: + - src + + inferPrivatePropertyTypeFromConstructor: true + + ignoreErrors: + - + identifier: missingType.return + - + identifier: missingType.iterableValue + - + identifier: missingType.parameter diff --git a/phpunit.xml.dist b/phpunit.xml.dist index e94cde70..a05da4c4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -14,6 +14,7 @@ + diff --git a/res/acmephp.conf.dist b/res/acmephp.conf.dist deleted file mode 100644 index 753a6b70..00000000 --- a/res/acmephp.conf.dist +++ /dev/null @@ -1,116 +0,0 @@ -# -# acmephp.conf - Configuration file for Acme PHP client. -# -# See https://github.com/acmephp/acmephp for more informations -# - -################################################################### -# Storage -# -# Configure here where and how you want to save your certificates -# and SSL keys. -# - -storage: - - # - # By default, Acme PHP will create a backup of every file - # before any modification. You can disable this mechanism here. - # - enable_backup: true - - # - # Actions to execute right after the generation of a file (key, CSR or certificate). - # Actions are executed in the order provided in configuration. - # - # See the documentation to learn how to create your own action. - # - post_generate: ~ - - # - # Build a file structure suited for nginx-proxy (https://github.com/jwilder/nginx-proxy) - # - # - action: build_nginxproxy - - # - # Mirror your local storage directory on a FTP host - # - # - action: mirror_file - # adapter: ftp - # root: /acmephp - # host: ftp.example.com - # username: username - # password: password - # # port: 21 - # # passive: true - # # ssl: true - # # timeout: 30 - - # - # Mirror your local storage directory on a SFTP host - # - # - action: mirror_file - # adapter: sftp - # root: /acmephp - # host: sftp.example.com - # username: username - # password: password - # # port: 22 - # # privateKey: path/to/or/contents/of/privatekey - # # timeout: 30 - - # - # Mirror your local storage directory on a an other location - # - # - action: mirror_file - # adapter: local - # root: /var/www/html - - # - # Install the certificate in an AWS ELB (Classic Load Balancer) - # - # - action: install_aws_elb - # region: eu-west-1 - # loadbalancer: my_elb - # # certificate_prefix: acmephp_ - # # cleanup_old_certificate: true - # # listener: 443 - - # - # Install the certificate in an AWS ELBv2 (Application Load Balancer) - # - # - action: install_aws_elbv2 - # region: eu-west-1 - # loadbalancer: my_elb - # # certificate_prefix: acmephp_ - # # cleanup_old_certificate: true - # # listener: 443 - -################################################################### -# Monitoring -# -# This section let you configure a simple monitoring mechanism that -# will warn you if an error occurs during a CRON job. -# - -monitoring: ~ # Monitoring is disabled by default - -# You can enabled it by configuring at least one alert handler. -# You can change the default handler level to decide when to be alerted -# (only when an error occurs or every time the CRON is started). -#monitoring: -# email: # Only SMTP(S) support for now -# to: galopintitouan@gmail.com -# host: smtp.example.com -# # port: 25 -# # username: ~ -# # password: ~ -# # encryption: ~ -# # subject: An error occured during Acme PHP CRON renewal -# # level: error # By default, only on error for email handler -# -# slack: -# token: your_token -# channel: general # Channel name without hashtag -# # username: Acme PHP -# # level: info # By default, on every CRON for slack handler diff --git a/src/Cli/Action/AbstractAction.php b/src/Cli/Action/AbstractAction.php index 51f19e18..8d497461 100644 --- a/src/Cli/Action/AbstractAction.php +++ b/src/Cli/Action/AbstractAction.php @@ -18,11 +18,7 @@ */ abstract class AbstractAction implements ActionInterface { - /** - * @param array $configuration - * @param array $keys - */ - protected function assertConfiguration($configuration, $keys) + protected function assertConfiguration(array $configuration, array $keys) { foreach ($keys as $key) { Assert::keyExists( diff --git a/src/Cli/Action/AbstractAwsAction.php b/src/Cli/Action/AbstractAwsAction.php index 0fd44209..24d2ddcc 100644 --- a/src/Cli/Action/AbstractAwsAction.php +++ b/src/Cli/Action/AbstractAwsAction.php @@ -32,10 +32,7 @@ public function __construct(ClientFactory $clientFactory) $this->clientFactory = $clientFactory; } - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) + public function handle(array $config, CertificateResponse $response) { $this->assertConfiguration($config, ['loadbalancer', 'region']); @@ -66,14 +63,12 @@ private function uploadCertificate(CertificateResponse $response, $region, $cert } $chainPem = implode("\n", $issuerChain); - $response = $iamClient->uploadServerCertificate( - [ - 'ServerCertificateName' => $certificateName, - 'CertificateBody' => $response->getCertificate()->getPEM(), - 'PrivateKey' => $response->getCertificateRequest()->getKeyPair()->getPrivateKey()->getPEM(), - 'CertificateChain' => $chainPem, - ] - ); + $response = $iamClient->uploadServerCertificate([ + 'ServerCertificateName' => $certificateName, + 'CertificateBody' => $response->getCertificate()->getPEM(), + 'PrivateKey' => $response->getCertificateRequest()->getKeyPair()->getPrivateKey()->getPEM(), + 'CertificateChain' => $chainPem, + ]); return $response['ServerCertificateMetadata']['Arn']; } @@ -89,7 +84,7 @@ private function cleanupOldCertificates($region, $certificatePrefix, $certificat ) { try { $this->retryCall( - // Try several time to delete certificate given AWS takes time to uninstall previous one + // Try several time to delete certificate given AWS takes time to uninstall previous one function () use ($iamClient, $certificate) { $iamClient->deleteServerCertificate( ['ServerCertificateName' => $certificate['ServerCertificateName']] diff --git a/src/Cli/Action/ActionInterface.php b/src/Cli/Action/ActionInterface.php index aac92f6b..0cfafe2e 100644 --- a/src/Cli/Action/ActionInterface.php +++ b/src/Cli/Action/ActionInterface.php @@ -21,8 +21,6 @@ interface ActionInterface /** * Get a certificate response and execute the action with it. * Use the given configuration if needed. - * - * @param array $config */ - public function handle($config, CertificateResponse $response); + public function handle(array $config, CertificateResponse $response); } diff --git a/src/Cli/Action/BuildNginxProxyAction.php b/src/Cli/Action/BuildNginxProxyAction.php index 60c91b2c..6377f27a 100644 --- a/src/Cli/Action/BuildNginxProxyAction.php +++ b/src/Cli/Action/BuildNginxProxyAction.php @@ -34,10 +34,7 @@ public function __construct(RepositoryInterface $repository) $this->repository = $repository; } - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) + public function handle(array $config, CertificateResponse $response) { $domain = $response->getCertificateRequest()->getDistinguishedName()->getCommonName(); $privateKey = $response->getCertificateRequest()->getKeyPair()->getPrivateKey(); diff --git a/src/Cli/Action/FilesystemAction.php b/src/Cli/Action/FilesystemAction.php index 75672799..0a8ab45e 100644 --- a/src/Cli/Action/FilesystemAction.php +++ b/src/Cli/Action/FilesystemAction.php @@ -14,7 +14,8 @@ use AcmePhp\Core\Filesystem\FilesystemFactoryInterface; use AcmePhp\Core\Filesystem\FilesystemInterface; use AcmePhp\Ssl\CertificateResponse; -use League\Flysystem\FilesystemInterface as FlysystemFilesystemInterface; +use League\Flysystem\FileAttributes; +use League\Flysystem\FilesystemOperator; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\ServiceLocator; @@ -24,29 +25,22 @@ class FilesystemAction extends AbstractAction { /** - * @var FlysystemFilesystemInterface + * @var FilesystemOperator */ - protected $master; + protected $storage; + /** * @var ContainerInterface */ protected $filesystemFactoryLocator; - /** - * @param ContainerInterface $filesystemFactoryLocator - */ - public function __construct( - FlysystemFilesystemInterface $master, - ContainerInterface $filesystemFactoryLocator = null - ) { - $this->filesystemFactoryLocator = $filesystemFactoryLocator = null ? new ServiceLocator([]) : $filesystemFactoryLocator; - $this->master = $master; + public function __construct(FilesystemOperator $storage, ?ContainerInterface $locator = null) + { + $this->storage = $storage; + $this->filesystemFactoryLocator = $locator ?: new ServiceLocator([]); } - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) + public function handle(array $config, CertificateResponse $response) { $this->assertConfiguration($config, ['adapter']); @@ -54,21 +48,18 @@ public function handle($config, CertificateResponse $response) $factory = $this->filesystemFactoryLocator->get($config['adapter']); $filesystem = $factory->create($config); - $files = $this->master->listContents('.', true); + $files = $this->storage->listContents('.', true); + /** @var FileAttributes $file */ foreach ($files as $file) { - if (0 === strpos($file['basename'], '.')) { + if (str_starts_with(basename($file->path()), '.')) { continue; } - $this->mirror($file['type'], $file['path'], $filesystem); + $this->mirror($file->type(), $file->path(), $filesystem); } } - /** - * @param string $type - * @param string $path - */ - private function mirror($type, $path, FilesystemInterface $filesystem) + private function mirror(string $type, string $path, FilesystemInterface $filesystem) { if ('dir' === $type) { $this->mirrorDirectory($path, $filesystem); @@ -79,25 +70,19 @@ private function mirror($type, $path, FilesystemInterface $filesystem) $this->mirrorFile($path, $filesystem); } - /** - * @param string $path - */ - private function mirrorDirectory($path, FilesystemInterface $filesystem) + private function mirrorDirectory(string $path, FilesystemInterface $filesystem) { $filesystem->createDir($path); } - /** - * @param string $path - */ - private function mirrorFile($path, FilesystemInterface $filesystem) + private function mirrorFile(string $path, FilesystemInterface $filesystem) { - $masterContent = $this->master->read($path); + $storageContent = $this->storage->read($path); - if (!\is_string($masterContent)) { - throw new \RuntimeException(sprintf('File %s could not be read on master storage', $path)); + if (!\is_string($storageContent)) { + throw new \RuntimeException(sprintf('File %s could not be read on storage storage', $path)); } - $filesystem->write($path, $masterContent); + $filesystem->write($path, $storageContent); } } diff --git a/src/Cli/Action/InstallAliyunCdnAction.php b/src/Cli/Action/InstallAliyunCdnAction.php index 6d50a13b..dcdea45d 100644 --- a/src/Cli/Action/InstallAliyunCdnAction.php +++ b/src/Cli/Action/InstallAliyunCdnAction.php @@ -22,10 +22,7 @@ */ class InstallAliyunCdnAction extends AbstractAction { - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) + public function handle(array $config, CertificateResponse $response) { $issuerChain = []; $issuerChain[] = $response->getCertificate()->getPEM(); diff --git a/src/Cli/Action/InstallAliyunWafAction.php b/src/Cli/Action/InstallAliyunWafAction.php index 678e883a..8ccff2ec 100644 --- a/src/Cli/Action/InstallAliyunWafAction.php +++ b/src/Cli/Action/InstallAliyunWafAction.php @@ -22,10 +22,7 @@ */ class InstallAliyunWafAction extends AbstractAction { - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) + public function handle(array $config, CertificateResponse $response) { $issuerChain = []; $issuerChain[] = $response->getCertificate()->getPEM(); diff --git a/src/Cli/Action/InstallCPanelAction.php b/src/Cli/Action/InstallCPanelAction.php new file mode 100644 index 00000000..cb7fef8d --- /dev/null +++ b/src/Cli/Action/InstallCPanelAction.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Cli\Action; + +use AcmePhp\Ssl\Certificate; +use AcmePhp\Ssl\CertificateResponse; +use GuzzleHttp\Client; + +class InstallCPanelAction extends AbstractAction +{ + /** + * @var Client + */ + private $httpClient; + + public function __construct(Client $httpClient) + { + $this->httpClient = $httpClient; + } + + public function handle(array $config, CertificateResponse $response) + { + $this->assertConfiguration($config, ['host', 'username', 'token']); + + $commonName = $response->getCertificateRequest()->getDistinguishedName()->getCommonName(); + $certificate = $response->getCertificate(); + $privateKey = $response->getCertificateRequest()->getKeyPair()->getPrivateKey(); + + $issuerChain = array_map(function (Certificate $certificate) { + return $certificate->getPEM(); + }, $certificate->getIssuerChain()); + + $this->installCertificate( + $config, + $commonName, + $certificate->getPEM(), + implode("\n", $issuerChain), + $privateKey->getPEM() + ); + } + + private function installCertificate($config, $domain, $crt, $caBundle, $key) + { + $this->httpClient->request('POST', $config['host'].'json-api/cpanel?'. + 'cpanel_jsonapi_apiversion=2&'. + 'cpanel_jsonapi_module=SSL&'. + 'cpanel_jsonapi_func=installssl&'. + 'domain='.$domain.'&'. + 'crt='.urlencode($crt).'&'. + 'key='.urlencode($key).'&'. + 'cabundle='.urlencode($caBundle), + ['headers' => ['Authorization' => 'cpanel '.$config['username'].':'.$config['token']], + ]); + } +} diff --git a/src/Cli/Action/PushFtpAction.php b/src/Cli/Action/PushFtpAction.php deleted file mode 100644 index 5001f72a..00000000 --- a/src/Cli/Action/PushFtpAction.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Action; - -use AcmePhp\Ssl\CertificateResponse; - -/** - * Action to write files using a Flysystem adapter. - * - * @author Titouan Galopin - */ -class PushFtpAction implements ActionInterface -{ - /** - * @var FilesystemAction - */ - private $filesystemAction; - - /** - * @param FilesystemAction - */ - public function __construct(FilesystemAction $filesystemAction) - { - @trigger_error('The "push_ftp" action is deprecated since version 1.0 and will be removed in 2.0. Use "mirror_file" action instead', E_USER_DEPRECATED); - - $this->filesystemAction = $filesystemAction; - } - - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) - { - $config['adapter'] = 'ftp'; - - return $this->filesystemAction->handle($config, $response); - } -} diff --git a/src/Cli/Action/PushRancherAction.php b/src/Cli/Action/PushRancherAction.php deleted file mode 100644 index 9b03c39b..00000000 --- a/src/Cli/Action/PushRancherAction.php +++ /dev/null @@ -1,131 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Action; - -use AcmePhp\Ssl\Certificate; -use AcmePhp\Ssl\CertificateResponse; -use GuzzleHttp\Client; -use GuzzleHttp\Psr7\Uri; - -/** - * Action to upload SSL certificates to Rancher using its API. - * - * @see http://docs.rancher.com/rancher/v1.2/en/api/api-resources/certificate/ - * - * @author Titouan Galopin - */ -class PushRancherAction implements ActionInterface -{ - /** - * @var Client - */ - private $httpClient; - - public function __construct(Client $httpClient) - { - $this->httpClient = $httpClient; - } - - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) - { - $payload = $this->createRancherPayloadFromResponse($response); - - $commonName = $response->getCertificateRequest()->getDistinguishedName()->getCommonName(); - $currentCertificates = $this->getRancherCertificates($config); - - $updated = false; - - foreach ($currentCertificates as $certificate) { - if ($certificate['name'] === $commonName) { - $updated = true; - $this->updateRancherCertificate($config, $certificate['id'], $payload); - } - } - - if (!$updated) { - $this->createRancherCertificate($config, $payload); - } - } - - private function createRancherPayloadFromResponse(CertificateResponse $response) - { - $certificate = $response->getCertificate(); - $privateKey = $response->getCertificateRequest()->getKeyPair()->getPrivateKey(); - - $issuerChain = array_map(function (Certificate $certificate) { - return $certificate->getPEM(); - }, $certificate->getIssuerChain()); - - return \GuzzleHttp\json_encode([ - 'name' => $response->getCertificateRequest()->getDistinguishedName()->getCommonName(), - 'description' => 'Generated with Acme PHP', - 'cert' => $certificate->getPEM(), - 'certChain' => implode("\n", $issuerChain), - 'key' => $privateKey->getPEM(), - ]); - } - - private function getRancherCertificates($config) - { - $nextPage = $this->createUrl($config, '/v1/certificates'); - $certificates = []; - - while ($nextPage) { - $page = $this->request('GET', $nextPage); - $certificates = array_merge($certificates, $page['data']); - - $nextPage = null; - if (isset($page['pagination'], $page['pagination']['next']) && \is_string($page['pagination']['next'])) { - $nextPage = $page['pagination']['next']; - } - } - - return $certificates; - } - - private function updateRancherCertificate($config, $previousCertificateId, $newPayload) - { - $this->request('PUT', $this->createUrl($config, '/v1/certificates/'.$previousCertificateId), $newPayload); - } - - private function createRancherCertificate($config, $payload) - { - $this->request('POST', $this->createUrl($config, '/v1/certificates'), $payload); - } - - private function createUrl($config, $endpoint) - { - $url = (new Uri()) - ->withScheme($config['ssl'] ? 'https' : 'http') - ->withUserInfo($config['access_key'], $config['secret_key']) - ->withHost($config['host']) - ->withPort($config['port']) - ->withPath($endpoint); - - return (string) $url; - } - - private function request($method, $url, $body = null) - { - $response = $this->httpClient->request($method, $url, [ - 'headers' => [ - 'Content-Type' => 'application/json', - ], - 'body' => $body ?: '', - ]); - - return \GuzzleHttp\json_decode(\GuzzleHttp\Psr7\copy_to_string($response->getBody()), true); - } -} diff --git a/src/Cli/Action/PushSftpAction.php b/src/Cli/Action/PushSftpAction.php deleted file mode 100644 index 18955271..00000000 --- a/src/Cli/Action/PushSftpAction.php +++ /dev/null @@ -1,47 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Action; - -use AcmePhp\Ssl\CertificateResponse; - -/** - * Action to write files using a Flysystem adapter. - * - * @author Titouan Galopin - */ -class PushSftpAction implements ActionInterface -{ - /** - * @var FilesystemAction - */ - private $filesystemAction; - - /** - * @param FilesystemAction - */ - public function __construct(FilesystemAction $filesystemAction) - { - @trigger_error('The "push_sftp" action is deprecated since version 1.0 and will be removed in 2.0. Use "mirror_file" action instead', E_USER_DEPRECATED); - - $this->filesystemAction = $filesystemAction; - } - - /** - * {@inheritdoc} - */ - public function handle($config, CertificateResponse $response) - { - $config['adapter'] = 'sftp'; - - return $this->filesystemAction->handle($config, $response); - } -} diff --git a/src/Cli/ActionHandler/ActionHandler.php b/src/Cli/ActionHandler/ActionHandler.php deleted file mode 100644 index a4d4f4a0..00000000 --- a/src/Cli/ActionHandler/ActionHandler.php +++ /dev/null @@ -1,83 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\ActionHandler; - -use AcmePhp\Cli\Exception\AcmeCliActionException; -use AcmePhp\Cli\Exception\AcmeCliException; -use AcmePhp\Ssl\CertificateResponse; -use Psr\Container\ContainerInterface; -use Psr\Log\LoggerInterface; - -/** - * @author Titouan Galopin - */ -class ActionHandler -{ - /** - * @var ContainerInterface - */ - private $actionLocator; - - /** - * @var LoggerInterface - */ - private $cliLogger; - - /** - * @var array - */ - private $postGenerateConfig; - - public function __construct(ContainerInterface $actionLocator, LoggerInterface $cliLogger, array $postGenerateConfig) - { - $this->actionLocator = $actionLocator; - $this->cliLogger = $cliLogger; - $this->postGenerateConfig = $postGenerateConfig; - } - - /** - * Apply all the registered actions to the given certificate response. - * - * @throws AcmeCliException if the configuration is invalid - * @throws AcmeCliActionException if there is a problem during the execution of an action - */ - public function handle(CertificateResponse $response) - { - $actions = []; - - // Prepare - foreach ($this->postGenerateConfig as $key => $actionConfig) { - if (empty($actionConfig['action'])) { - throw new AcmeCliException(sprintf('No action was configured at key storage.post_generate.%s, a non-empty "action" key is required.', $key)); - } - - $name = $actionConfig['action']; - unset($actionConfig['action']); - - $actions[] = [ - 'handler' => $this->actionLocator->get($name), - 'name' => $name, - 'config' => $actionConfig, - ]; - } - - // Handle - foreach ($actions as $action) { - try { - $this->cliLogger->info(' - Running '.$action['name'].'...'); - $action['handler']->handle($action['config'], $response); - } catch (\Exception $exception) { - throw new AcmeCliActionException($action['name'], $exception); - } - } - } -} diff --git a/src/Cli/Application.php b/src/Cli/Application.php index 8de32692..58ecbb8e 100644 --- a/src/Cli/Application.php +++ b/src/Cli/Application.php @@ -11,57 +11,54 @@ namespace AcmePhp\Cli; -use AcmePhp\Cli\Command\AuthorizeCommand; -use AcmePhp\Cli\Command\CheckCommand; use AcmePhp\Cli\Command\Helper\DistinguishedNameHelper; -use AcmePhp\Cli\Command\MonitoringTestCommand; -use AcmePhp\Cli\Command\RegisterCommand; -use AcmePhp\Cli\Command\RequestCommand; use AcmePhp\Cli\Command\RevokeCommand; use AcmePhp\Cli\Command\RunCommand; -use AcmePhp\Cli\Command\SelfUpdateCommand; use AcmePhp\Cli\Command\StatusCommand; +use SelfUpdate\SelfUpdateCommand; +use SelfUpdate\SelfUpdateManager; use Symfony\Component\Console\Application as BaseApplication; -use Symfony\Component\Console\Input\InputOption; -use Webmozart\PathUtil\Path; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Filesystem\Path; /** * @author Titouan Galopin */ class Application extends BaseApplication { - const VERSION = '1.2.0'; + public const PROVIDERS = [ + 'letsencrypt' => 'https://acme-v02.api.letsencrypt.org/directory', + 'zerossl' => 'https://acme.zerossl.com/v2/DV90', + 'localhost' => 'https://localhost:14000/dir', + ]; - /** - * {@inheritdoc} - */ public function __construct() { - parent::__construct('Acme PHP - Let\'s Encrypt client', self::VERSION); + // This is replaced by humbug/box with a string that looks like this: x.y.z@tag + parent::__construct('Acme PHP - Let\'s Encrypt/ZeroSSL client', '@git@'); } - /** - * {@inheritdoc} - */ - protected function getDefaultCommands() + protected function getDefaultCommands(): array { + $version = explode('@', $this->getVersion())[0]; + + if (class_exists(SelfUpdateManager::class)) { + $selfUpdateCommand = new SelfUpdateCommand(new SelfUpdateManager($this->getName(), '' === $version ? '0.0.0' : $version, 'acmephp/acmephp')); + } else { + // Support for older versions of the self-update package + // @phpstan-ignore-next-line + $selfUpdateCommand = new SelfUpdateCommand($this->getName(), '' === $version ? '0.0.0' : $version, 'acmephp/acmephp'); + } + return array_merge(parent::getDefaultCommands(), [ new RunCommand(), - new RegisterCommand(), - new AuthorizeCommand(), - new CheckCommand(), - new RequestCommand(), new RevokeCommand(), new StatusCommand(), - new SelfUpdateCommand(), - new MonitoringTestCommand(), + $selfUpdateCommand, ]); } - /** - * {@inheritdoc} - */ - protected function getDefaultHelperSet() + protected function getDefaultHelperSet(): HelperSet { $set = parent::getDefaultHelperSet(); $set->set(new DistinguishedNameHelper()); @@ -69,40 +66,6 @@ protected function getDefaultHelperSet() return $set; } - /** - * {@inheritdoc} - */ - protected function getDefaultInputDefinition() - { - $definition = parent::getDefaultInputDefinition(); - - $definition->addOption(new InputOption( - 'server', - null, - InputOption::VALUE_REQUIRED, - 'Set the ACME server directory to use', - 'https://acme-v02.api.letsencrypt.org/directory' - )); - - return $definition; - } - - /** - * @return string - */ - public function getConfigFile() - { - return Path::canonicalize('~/.acmephp/acmephp.conf'); - } - - /** - * @return string - */ - public function getConfigReferenceFile() - { - return Path::canonicalize(__DIR__.'/../../res/acmephp.conf.dist'); - } - /** * @return string */ @@ -110,12 +73,4 @@ public function getStorageDirectory() { return Path::canonicalize('~/.acmephp/master'); } - - /** - * @return string - */ - public function getBackupDirectory() - { - return Path::canonicalize('~/.acmephp/backup'); - } } diff --git a/src/Cli/Aws/ClientFactory.php b/src/Cli/Aws/ClientFactory.php index 76754b28..7117e785 100644 --- a/src/Cli/Aws/ClientFactory.php +++ b/src/Cli/Aws/ClientFactory.php @@ -17,19 +17,19 @@ class ClientFactory { - public function getIamClient($region = null) + public function getIamClient($region = null): IamClient { return new IamClient($this->getClientArgs(['region' => $region, 'version' => '2010-05-08'])); } - public function getElbClient($region = null) + public function getElbClient($region = null): ElasticLoadBalancingClient { return new ElasticLoadBalancingClient( $this->getClientArgs(['region' => $region, 'version' => '2012-06-01']) ); } - public function getElbv2Client($region = null) + public function getElbv2Client($region = null): ElasticLoadBalancingV2Client { return new ElasticLoadBalancingV2Client( $this->getClientArgs(['region' => $region, 'version' => '2015-12-01']) diff --git a/src/Cli/Command/AbstractCommand.php b/src/Cli/Command/AbstractCommand.php index cf8c0951..8d27d6e6 100644 --- a/src/Cli/Command/AbstractCommand.php +++ b/src/Cli/Command/AbstractCommand.php @@ -11,17 +11,13 @@ namespace AcmePhp\Cli\Command; -use AcmePhp\Cli\ActionHandler\ActionHandler; -use AcmePhp\Cli\Application; -use AcmePhp\Cli\Configuration\AcmeConfiguration; use AcmePhp\Cli\Exception\CommandFlowException; -use AcmePhp\Cli\Repository\RepositoryV2Interface; +use AcmePhp\Cli\Repository\RepositoryInterface; use AcmePhp\Core\AcmeClient; use AcmePhp\Core\Challenge\Dns\LibDnsResolver; use AcmePhp\Core\Http\SecureHttpClient; use AcmePhp\Ssl\Signer\CertificateRequestSigner; use Psr\Log\LoggerInterface; -use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Config\FileLocator; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; @@ -30,14 +26,11 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\Filesystem\Exception\IOException; -use Symfony\Component\Filesystem\Filesystem; -use Symfony\Component\Yaml\Yaml; /** * @author Titouan Galopin */ -abstract class AbstractCommand extends Command implements LoggerInterface +abstract class AbstractCommand extends Command { /** * @var InputInterface @@ -49,49 +42,25 @@ abstract class AbstractCommand extends Command implements LoggerInterface */ protected $output; - /** - * @var array|null - */ - private $configuration; - /** * @var ContainerBuilder|null */ private $container; - /** - * {@inheritdoc} - */ - protected function initialize(InputInterface $input, OutputInterface $output) + protected function initialize(InputInterface $input, OutputInterface $output): void { $this->input = $input; $this->output = $output; } - /** - * @return RepositoryV2Interface - */ - protected function getRepository() + protected function getRepository(): RepositoryInterface { $this->debug('Loading repository'); return $this->getContainer()->get('repository'); } - /** - * @return ActionHandler - */ - protected function getActionHandler() - { - $this->debug('Loading action handler'); - - return $this->getContainer()->get('acmephp.action_handler'); - } - - /** - * @return AcmeClient - */ - protected function getClient() + protected function getClient(string $directoryUrl): AcmeClient { $this->debug('Creating Acme client'); $this->notice('Loading account key pair...'); @@ -107,21 +76,15 @@ protected function getClient() /** @var CertificateRequestSigner $csrSigner */ $csrSigner = $this->getContainer()->get('ssl.csr_signer'); - return new AcmeClient($httpClient, $this->input->getOption('server'), $csrSigner); + return new AcmeClient($httpClient, $directoryUrl, $csrSigner); } - /** - * @return LoggerInterface - */ - protected function getCliLogger() + protected function getCliLogger(): LoggerInterface { return $this->getContainer()->get('cli_logger'); } - /** - * @return ContainerBuilder - */ - protected function getContainer() + protected function getContainer(): ContainerBuilder { if (null === $this->container) { $this->initializeContainer(); @@ -132,25 +95,12 @@ protected function getContainer() private function initializeContainer() { - if (null === $this->configuration) { - $this->initializeConfiguration(); - } - $this->container = new ContainerBuilder(); // Application services and parameters $this->container->set('app', $this->getApplication()); $this->container->set('container', $this->container); - $this->container->setParameter('app.version', Application::VERSION); $this->container->setParameter('app.storage_directory', $this->getApplication()->getStorageDirectory()); - $this->container->setParameter('app.backup_directory', $this->getApplication()->getBackupDirectory()); - - // Load configuration - $processor = new Processor(); - $config = $processor->processConfiguration(new AcmeConfiguration(), $this->configuration); - $this->container->setParameter('storage.enable_backup', $config['storage']['enable_backup']); - $this->container->setParameter('storage.post_generate', $config['storage']['post_generate']); - $this->container->setParameter('monitoring.handlers', $config['monitoring']); // Load services $loader = new XmlFileLoader($this->container, new FileLocator(__DIR__.'/../Resources')); @@ -181,94 +131,48 @@ private function initializeContainer() $this->container->set('output', $this->output); } - private function initializeConfiguration() - { - $configFile = $this->getApplication()->getConfigFile(); - $referenceFile = $this->getApplication()->getConfigReferenceFile(); - - if (!file_exists($configFile)) { - $filesystem = new Filesystem(); - $filesystem->dumpFile($configFile, file_get_contents($referenceFile)); - - $this->notice('Configuration file '.$configFile.' did not exist and has been created.'); - } - - if (!is_readable($configFile)) { - throw new IOException('Configuration file '.$configFile.' is not readable.'); - } - - $this->configuration = ['acmephp' => Yaml::parse(file_get_contents($configFile))]; - } - - /** - * {@inheritdoc} - */ public function emergency($message, array $context = []) { - return $this->getCliLogger()->emergency($message, $context); + $this->getCliLogger()->emergency($message, $context); } - /** - * {@inheritdoc} - */ public function alert($message, array $context = []) { - return $this->getCliLogger()->alert($message, $context); + $this->getCliLogger()->alert($message, $context); } - /** - * {@inheritdoc} - */ public function critical($message, array $context = []) { - return $this->getCliLogger()->critical($message, $context); + $this->getCliLogger()->critical($message, $context); } - /** - * {@inheritdoc} - */ public function error($message, array $context = []) { - return $this->getCliLogger()->error($message, $context); + $this->getCliLogger()->error($message, $context); } - /** - * {@inheritdoc} - */ public function warning($message, array $context = []) { - return $this->getCliLogger()->warning($message, $context); + $this->getCliLogger()->warning($message, $context); } - /** - * {@inheritdoc} - */ public function notice($message, array $context = []) { - return $this->getCliLogger()->notice($message, $context); + $this->getCliLogger()->notice($message, $context); } - /** - * {@inheritdoc} - */ public function info($message, array $context = []) { - return $this->getCliLogger()->info($message, $context); + $this->getCliLogger()->info($message, $context); } - /** - * {@inheritdoc} - */ public function debug($message, array $context = []) { - return $this->getCliLogger()->debug($message, $context); + $this->getCliLogger()->debug($message, $context); } - /** - * {@inheritdoc} - */ public function log($level, $message, array $context = []) { - return $this->getCliLogger()->log($level, $message, $context); + $this->getCliLogger()->log($level, $message, $context); } } diff --git a/src/Cli/Command/AuthorizeCommand.php b/src/Cli/Command/AuthorizeCommand.php deleted file mode 100644 index c767b0d1..00000000 --- a/src/Cli/Command/AuthorizeCommand.php +++ /dev/null @@ -1,138 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Command; - -use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface; -use AcmePhp\Core\Challenge\SolverInterface; -use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; -use AcmePhp\Core\Protocol\AuthorizationChallenge; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @author Titouan Galopin - */ -class AuthorizeCommand extends AbstractCommand -{ - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->setName('authorize') - ->setDefinition([ - new InputOption('solver', 's', InputOption::VALUE_REQUIRED, 'The type of challenge solver to use (available: http, dns, route53, gandi)', 'http'), - new InputArgument('domains', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of domains to ask an authorization for'), - ]) - ->setDescription('Ask the ACME server for an authorization token to check you are the owner of a domain') - ->setHelp(<<<'EOF' -The %command.name% command asks the ACME server for an authorization token. -You will then have to expose that token on a specific URL under that domain and ask for -the server to check you are the own of the domain by checking this URL. - -Ask the server for an authorization token: - - php %command.full_name% example.com www.exemple.org *.example.io - -Follow the instructions to expose your token on the specific URL, and then run the check -command to tell the server to check your token. -EOF - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->error('This command is deprecated. Use command "run" instead'); - - $client = $this->getClient(); - $domains = $input->getArgument('domains'); - - $solverName = strtolower($input->getOption('solver')); - - $this->debug('Locating solver', ['name' => $solverName]); - - $solverLocator = $this->getContainer()->get('acmephp.challenge_solver.locator'); - /** @var SolverInterface $solver */ - $solver = $solverLocator->get($solverName); - $this->debug('Solver found', ['name' => $solverName]); - - $this->notice(sprintf('Requesting an authorization token for domains %s ...', implode(', ', $domains))); - $order = $client->requestOrder($domains); - $this->notice('The authorization tokens was successfully fetched!'); - $authorizationChallengesToSolve = []; - foreach ($order->getAuthorizationsChallenges() as $domainKey => $authorizationChallenges) { - $authorizationChallenge = null; - foreach ($authorizationChallenges as $candidate) { - if ($solver->supports($candidate)) { - $authorizationChallenge = $candidate; - - $this->debug('Authorization challenge supported by solver', [ - 'solver' => $solverName, - 'challenge' => $candidate->getType(), - ]); - - break; - } - - $this->debug('Authorization challenge not supported by solver', [ - 'solver' => $solverName, - 'challenge' => $candidate->getType(), - ]); - } - if (null === $authorizationChallenge) { - throw new ChallengeNotSupportedException(); - } - $this->debug('Storing authorization challenge', [ - 'domain' => $domainKey, - 'challenge' => $authorizationChallenge->toArray(), - ]); - - $this->getRepository()->storeDomainAuthorizationChallenge($domainKey, $authorizationChallenge); - $authorizationChallengesToSolve[] = $authorizationChallenge; - } - if ($solver instanceof MultipleChallengesSolverInterface) { - $solver->solveAll($authorizationChallengesToSolve); - } else { - /** @var AuthorizationChallenge $authorizationChallenge */ - foreach ($authorizationChallengesToSolve as $authorizationChallenge) { - $this->info('Solving authorization challenge for domain', [ - 'domain' => $authorizationChallenge->getDomain(), - 'challenge' => $authorizationChallenge->toArray(), - ]); - $solver->solve($authorizationChallenge); - } - } - - $this->getRepository()->storeCertificateOrder($domains, $order); - - $this->info(sprintf( -<<<'EOF' -Then, you can ask to the CA to check the challenge! - Call the check command to ask the server to check your URL: - - php %s check -s %s %s - -EOF - , - $_SERVER['PHP_SELF'], - $solverName, - implode(' ', array_keys($order->getAuthorizationsChallenges())) - )); - - return 0; - } -} diff --git a/src/Cli/Command/CheckCommand.php b/src/Cli/Command/CheckCommand.php deleted file mode 100644 index 200d5ae1..00000000 --- a/src/Cli/Command/CheckCommand.php +++ /dev/null @@ -1,155 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Command; - -use AcmePhp\Cli\Exception\CommandFlowException; -use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface; -use AcmePhp\Core\Challenge\SolverInterface; -use AcmePhp\Core\Challenge\ValidatorInterface; -use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; -use AcmePhp\Core\Protocol\AuthorizationChallenge; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @author Titouan Galopin - */ -class CheckCommand extends AbstractCommand -{ - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->setName('check') - ->setDefinition([ - new InputOption('solver', 's', InputOption::VALUE_REQUIRED, 'The type of challenge solver to use (available: http, dns, route53, gandi)', 'http'), - new InputOption('no-test', 't', InputOption::VALUE_NONE, 'Whether or not internal tests should be disabled'), - new InputArgument('domains', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'The list of domains to check the authorization for'), - ]) - ->setDescription('Ask the ACME server to check an authorization token you expose to prove you are the owner of a list of domains') - ->setHelp(<<<'EOF' -The %command.name% command asks the ACME server to check an authorization token -you exposed to prove you own a given list of domains. - -Once you are the proved owner of the domains, you can request SSL certificates for those domains. - -Use the authorize command before this one. -EOF - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->error('This command is deprecated. Use command "run" instead'); - - $repository = $this->getRepository(); - $client = $this->getClient(); - $domains = $input->getArgument('domains'); - - $solverName = strtolower($input->getOption('solver')); - - $this->debug('Locating solver', ['name' => $solverName]); - - $solverLocator = $this->getContainer()->get('acmephp.challenge_solver.locator'); - /** @var SolverInterface $solver */ - $solver = $solverLocator->get($solverName); - - $this->debug('Solver found', ['name' => $solverName]); - - /** @var ValidatorInterface $validator */ - $validator = $this->getContainer()->get('challenge_validator'); - - $this->notice(sprintf('Loading the order related to the domains %s ...', implode(', ', $domains))); - $order = null; - if ($this->getRepository()->hasCertificateOrder($domains)) { - $order = $this->getRepository()->loadCertificateOrder($domains); - } - - $this->notice(sprintf('Loading the authorization token for domains %s ...', implode(', ', $domains))); - $authorizationChallengeToCleanup = []; - foreach ($domains as $domain) { - if ($order) { - $authorizationChallenge = null; - $authorizationChallenges = $order->getAuthorizationChallenges($domain); - foreach ($authorizationChallenges as $challenge) { - if ($solver->supports($challenge)) { - $authorizationChallenge = $challenge; - break; - } - } - if (null === $authorizationChallenge) { - throw new ChallengeNotSupportedException(); - } - } else { - if (!$repository->hasDomainAuthorizationChallenge($domain)) { - throw new CommandFlowException('ask a challenge', 'authorize', [$domains]); - } - $authorizationChallenge = $repository->loadDomainAuthorizationChallenge($domain); - if (!$solver->supports($authorizationChallenge)) { - throw new ChallengeNotSupportedException(); - } - } - $this->debug('Challenge loaded', ['challenge' => $authorizationChallenge->toArray()]); - - $authorizationChallenge = $client->reloadAuthorization($authorizationChallenge); - if ($authorizationChallenge->isValid()) { - $this->notice(sprintf('The challenge is alread validated for domain %s ...', $domain)); - } else { - if (!$input->getOption('no-test')) { - $this->notice(sprintf('Testing the challenge for domain %s...', $domain)); - if (!$validator->isValid($authorizationChallenge)) { - $this->output->writeln(sprintf('Can not valid challenge for domain %s ...', $domain)); - } - } - - $this->notice(sprintf('Requesting authorization check for domain %s ...', $domain)); - $client->challengeAuthorization($authorizationChallenge); - $authorizationChallengeToCleanup[] = $authorizationChallenge; - } - } - - $this->info(sprintf(<<<'EOF' - -The authorization check was successful! - -You are now the proved owner of those domains %s. -Please note that you won't need to prove it anymore as long as you keep the same account key pair. - -You can now request a certificate for your domains: - - php %s request %s - -EOF - , - implode(', ', $domains), - $_SERVER['PHP_SELF'], - implode(' -a ', $domains) - )); - - if ($solver instanceof MultipleChallengesSolverInterface) { - $solver->cleanupAll($authorizationChallengeToCleanup); - } else { - /** @var AuthorizationChallenge $authorizationChallenge */ - foreach ($authorizationChallengeToCleanup as $authorizationChallenge) { - $solver->cleanup($authorizationChallenge); - } - } - - return 0; - } -} diff --git a/src/Cli/Command/Helper/DistinguishedNameHelper.php b/src/Cli/Command/Helper/DistinguishedNameHelper.php index cb34c4bf..daa964da 100644 --- a/src/Cli/Command/Helper/DistinguishedNameHelper.php +++ b/src/Cli/Command/Helper/DistinguishedNameHelper.php @@ -23,10 +23,7 @@ */ class DistinguishedNameHelper extends Helper { - /** - * {@inheritdoc} - */ - public function getName() + public function getName(): string { return 'distinguished_name'; } diff --git a/src/Cli/Command/MonitoringTestCommand.php b/src/Cli/Command/MonitoringTestCommand.php deleted file mode 100644 index 80e04bb1..00000000 --- a/src/Cli/Command/MonitoringTestCommand.php +++ /dev/null @@ -1,73 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Command; - -use AcmePhp\Cli\Exception\AcmeCliException; -use AcmePhp\Cli\Monitoring\HandlerBuilderInterface; -use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @author Titouan Galopin - */ -class MonitoringTestCommand extends AbstractCommand -{ - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->setName('monitoring-test') - ->setDefinition([ - new InputArgument('level', InputArgument::OPTIONAL, 'The level to use for the test (info/error, by default error)', 'error'), - ]) - ->setDescription('Throw an error in a monitored context to test your configuration') - ->setHelp(<<<'EOF' -The %command.name% command list will set up the same monitored context as in your CRON -jobs and will voluntarily throw an error inside it so you can check you are successfully alerted if -there is a problem. -EOF - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->info('Loading monitoring configuration...'); - - /** @var LoggerInterface $monitoringLogger */ - $monitoringLogger = $this->getContainer()->get('acmephp.monitoring_factory')->createLogger(); - - $level = $input->getArgument('level'); - - if (!\in_array($level, [HandlerBuilderInterface::LEVEL_ERROR, HandlerBuilderInterface::LEVEL_INFO], true)) { - throw new AcmeCliException('Level '.$level.' is not valid (available levels: info, error)'); - } - - $this->info('Triggering monitoring on "'.$level.'" level...'); - - if (HandlerBuilderInterface::LEVEL_INFO === $level) { - $monitoringLogger->info('This is a testing message from Acme PHP monitoring (info level)'); - } else { - $monitoringLogger->alert('This is a testing message from Acme PHP monitoring (error level)'); - } - - $this->notice('Triggered successfully'); - $this->info('You should have been alerted'); - - return 0; - } -} diff --git a/src/Cli/Command/RegisterCommand.php b/src/Cli/Command/RegisterCommand.php deleted file mode 100644 index de703367..00000000 --- a/src/Cli/Command/RegisterCommand.php +++ /dev/null @@ -1,96 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Command; - -use AcmePhp\Cli\Command\Helper\KeyOptionCommandTrait; -use AcmePhp\Ssl\KeyPair; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @author Titouan Galopin - */ -class RegisterCommand extends AbstractCommand -{ - use KeyOptionCommandTrait; - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->setName('register') - ->setDefinition([ - new InputArgument('email', InputArgument::OPTIONAL, 'An e-mail to use when certificates will expire soon'), - new InputOption('agreement', null, InputOption::VALUE_REQUIRED, '[DEPRECATED] The server usage conditions you agree with (automatically agreed with all licenses)'), - new InputOption('key-type', 'k', InputOption::VALUE_REQUIRED, 'The type of private key used to sign certificates (one of RSA, EC)', 'RSA'), - ]) - ->setDescription('Register your account private key in the ACME server') - ->setHelp(<<<'EOF' -The %command.name% command register your account key in the ACME server -provided by the option --server (by default it will use Let's Encrypt servers). -This command will generate an account key if no account key exists in the storage. - -You can add an e-mail that will be added to your registration (required for Let's Encrypt): - - php %command.full_name% acmephp@example.com -EOF - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->error('This command is deprecated. Use command "run" instead'); - - $repository = $this->getRepository(); - - /* - * Generate account key pair if needed - */ - if (!$repository->hasAccountKeyPair()) { - $this->notice('No account key pair was found, generating one...'); - $this->debug('Generating a key pair'); - - /* @var KeyPair $accountKeyPair */ - $accountKeyPair = $this->getContainer()->get('ssl.key_pair_generator')->generateKeyPair( - $this->createKeyOption($input->getOption('key-type')) - ); - - $this->debug('Key pair generated, storing', ['public_key' => $accountKeyPair->getPublicKey()->getPEM()]); - $repository->storeAccountKeyPair($accountKeyPair); - } - - /* - * Register on server - */ - $client = $this->getClient(); - - $email = $input->getArgument('email') ?: null; - if ($input->getOption('agreement')) { - @trigger_error('The "agreement" option is deprecated since version 1.0 and will be removed in 2.0.', E_USER_DEPRECATED); - } - - $this->notice('Registering on the ACME server...'); - $this->debug('Registering your account on Acme server', ['email' => $email]); - - $client->registerAccount(null, $email); - - $this->notice('Account registered successfully!'); - - return 0; - } -} diff --git a/src/Cli/Command/RequestCommand.php b/src/Cli/Command/RequestCommand.php deleted file mode 100644 index d2845bbb..00000000 --- a/src/Cli/Command/RequestCommand.php +++ /dev/null @@ -1,404 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Command; - -use AcmePhp\Cli\ActionHandler\ActionHandler; -use AcmePhp\Cli\Command\Helper\DistinguishedNameHelper; -use AcmePhp\Cli\Command\Helper\KeyOptionCommandTrait; -use AcmePhp\Cli\Exception\CommandFlowException; -use AcmePhp\Cli\Repository\Repository; -use AcmePhp\Cli\Repository\RepositoryInterface; -use AcmePhp\Core\AcmeClientV2Interface; -use AcmePhp\Ssl\CertificateRequest; -use AcmePhp\Ssl\CertificateResponse; -use AcmePhp\Ssl\DistinguishedName; -use AcmePhp\Ssl\KeyPair; -use AcmePhp\Ssl\ParsedCertificate; -use Psr\Log\LoggerInterface; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * @author Titouan Galopin - */ -class RequestCommand extends AbstractCommand -{ - use KeyOptionCommandTrait; - - /** - * @var RepositoryInterface - */ - private $repository; - - /** - * @var AcmeClientV2Interface - */ - private $client; - - /** - * @var ActionHandler - */ - private $actionHandler; - - /** - * {@inheritdoc} - */ - protected function configure() - { - $this->setName('request') - ->setDefinition([ - new InputArgument('domain', InputArgument::REQUIRED, 'The domain to get a certificate for'), - new InputOption('force', 'f', InputOption::VALUE_NONE, 'Whether to force renewal or not (by default, renewal will be done only if the certificate expire in less than a week)'), - new InputOption('country', null, InputOption::VALUE_REQUIRED, 'Your country two-letters code (field "C" of the distinguished name, for instance: "US")'), - new InputOption('province', null, InputOption::VALUE_REQUIRED, 'Your country province (field "ST" of the distinguished name, for instance: "California")'), - new InputOption('locality', null, InputOption::VALUE_REQUIRED, 'Your locality (field "L" of the distinguished name, for instance: "Mountain View")'), - new InputOption('organization', null, InputOption::VALUE_REQUIRED, 'Your organization/company (field "O" of the distinguished name, for instance: "Acme PHP")'), - new InputOption('unit', null, InputOption::VALUE_REQUIRED, 'Your unit/department in your organization (field "OU" of the distinguished name, for instance: "Sales")'), - new InputOption('email', null, InputOption::VALUE_REQUIRED, 'Your e-mail address (field "E" of the distinguished name)'), - new InputOption('alternative-name', 'a', InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Alternative domains for this certificate'), - new InputOption('key-type', 'k', InputOption::VALUE_REQUIRED, 'The type of private key used to sign certificates (one of RSA, EC)', 'RSA'), - ]) - ->setDescription('Request a SSL certificate for a domain') - ->setHelp(<<<'EOF' -The %command.name% command requests to the ACME server a SSL certificate for a -given domain. - -This certificate will be stored in the Acme PHP storage directory. - -You need to be the proved owner of the domain you ask a certificate for. To prove your ownership -of the domain, please use commands authorize and check before this one. -EOF - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->error('This command is deprecated. Use command "run" instead'); - - $this->repository = $this->getRepository(); - $this->client = $this->getClient(); - $this->actionHandler = $this->getActionHandler(); - - $domain = $input->getArgument('domain'); - $alternativeNames = array_unique($input->getOption('alternative-name')); - sort($alternativeNames); - - // Certificate renewal - if ($this->hasValidCertificate($domain, $alternativeNames)) { - $this->debug('Certificate found, executing renewal', [ - 'domain' => $domain, - 'alternative_names' => $alternativeNames, - ]); - - return $this->executeRenewal($domain, $alternativeNames); - } - - $this->debug('No certificate found, executing first request', [ - 'domain' => $domain, - 'alternative_names' => $alternativeNames, - ]); - - // Certificate first request - return $this->executeFirstRequest($domain, $alternativeNames, $input->getOption('key-type')); - } - - private function hasValidCertificate($domain, array $alternativeNames) - { - if (!$this->repository->hasDomainCertificate($domain)) { - return false; - } - - if (!$this->repository->hasDomainKeyPair($domain)) { - return false; - } - - if (!$this->repository->hasDomainDistinguishedName($domain)) { - return false; - } - - if ($this->repository->loadDomainDistinguishedName($domain)->getSubjectAlternativeNames() !== $alternativeNames) { - return false; - } - - return true; - } - - /** - * Request a first certificate for the given domain. - * - * @param string $domain - * @param string $keyType - */ - private function executeFirstRequest($domain, array $alternativeNames, $keyType) - { - $introduction = <<<'EOF' - -There is currently no certificate for domain %s in the Acme PHP storage. As it is the -first time you request a certificate for this domain, some configuration is required. - -Generating domain key pair... -EOF; - - $this->info(sprintf($introduction, $domain)); - - /* @var KeyPair $domainKeyPair */ - $domainKeyPair = $this->getContainer()->get('ssl.key_pair_generator')->generateKeyPair( - $this->createKeyOption($keyType) - ); - $this->repository->storeDomainKeyPair($domain, $domainKeyPair); - - $this->debug('Domain key pair generated and stored', [ - 'domain' => $domain, - 'public_key' => $domainKeyPair->getPublicKey()->getPEM(), - ]); - - $distinguishedName = $this->getOrCreateDistinguishedName($domain, $alternativeNames); - $this->notice('Distinguished name informations have been stored locally for this domain (they won\'t be asked on renewal).'); - - // Order - $domains = array_merge([$domain], $alternativeNames); - $this->notice(sprintf('Loading the order related to the domains %s ...', implode(', ', $domains))); - if (!$this->getRepository()->hasCertificateOrder($domains)) { - throw new CommandFlowException('ask a challenge', 'authorize', $domains); - } - $order = $this->getRepository()->loadCertificateOrder($domains); - - // Request - $this->notice(sprintf('Requesting first certificate for domain %s ...', $domain)); - $csr = new CertificateRequest($distinguishedName, $domainKeyPair); - $response = $this->client->finalizeOrder($order, $csr); - $this->debug('Certificate received', ['certificate' => $response->getCertificate()->getPEM()]); - - // Store - $this->repository->storeDomainCertificate($domain, $response->getCertificate()); - $this->debug('Certificate stored'); - - // Post-generate actions - $this->notice('Running post-generate actions...'); - $this->actionHandler->handle($response); - - // Success message - /** @var ParsedCertificate $parsedCertificate */ - $parsedCertificate = $this->getContainer()->get('ssl.certificate_parser')->parse($response->getCertificate()); - - $success = <<<'EOF' - -The SSL certificate was fetched successfully! - -This certificate is valid from now to %expiration%. - -5 files were created in the Acme PHP storage directory: - - * %private% contains your domain private key (required in many cases). - - * %cert% contains only your certificate, without the issuer certificate. - It may be useful in certains cases but you will probably not need it (use fullchain.pem instead). - - * %chain% contains the issuer certificate chain (its certificate, the - certificate of its issuer, the certificate of the issuer of its issuer, etc.). Your certificate is - not present in this file. - - * %fullchain% contains your certificate AND the issuer certificate chain. - You most likely will use this file in your webserver. - - * %combined% contains the fullchain AND your domain private key (some - webservers expect this format such as haproxy). - -Read the documentation at https://acmephp.github.io/documentation/ to learn more about how to -configure your web server and set up automatic renewal. - -To renew your certificate manually, simply re-run this command. - -EOF; - - $masterPath = $this->getContainer()->getParameter('app.storage_directory'); - - $replacements = [ - '%expiration%' => $parsedCertificate->getValidTo()->format(\DateTime::ISO8601), - '%private%' => $masterPath.'/'.Repository::PATH_DOMAIN_KEY_PRIVATE, - '%combined%' => $masterPath.'/'.Repository::PATH_DOMAIN_CERT_COMBINED, - '%cert%' => $masterPath.'/'.Repository::PATH_DOMAIN_CERT_CERT, - '%chain%' => $masterPath.'/'.Repository::PATH_DOMAIN_CERT_CHAIN, - '%fullchain%' => $masterPath.'/'.Repository::PATH_DOMAIN_CERT_FULLCHAIN, - ]; - - $this->info(strtr(strtr($success, $replacements), ['{domain}' => $domain])); - - return 0; - } - - /** - * Renew a given domain certificate. - * - * @param string $domain - */ - private function executeRenewal($domain, array $alternativeNames) - { - /** @var LoggerInterface $monitoringLogger */ - $monitoringLogger = $this->getContainer()->get('acmephp.monitoring_factory')->createLogger(); - - try { - // Check expiration date to avoid too much renewal - $this->debug('Loading current certificate', [ - 'domain' => $domain, - ]); - - $certificate = $this->repository->loadDomainCertificate($domain); - - if (!$this->input->getOption('force')) { - /** @var ParsedCertificate $parsedCertificate */ - $parsedCertificate = $this->getContainer()->get('ssl.certificate_parser')->parse($certificate); - - if ($parsedCertificate->getValidTo()->format('U') - time() >= 604800) { - $monitoringLogger->debug('Certificate does not need renewal', [ - 'domain' => $domain, - 'valid_until' => $parsedCertificate->getValidTo()->format('Y-m-d H:i:s'), - ]); - - $this->notice(sprintf( - 'Current certificate is valid until %s, renewal is not necessary. Use --force to force renewal.', - $parsedCertificate->getValidTo()->format('Y-m-d H:i:s')) - ); - - // Post-generate actions - $this->info('Running post-generate actions...'); - $response = new CertificateResponse( - new CertificateRequest( - $this->repository->loadDomainDistinguishedName($domain), - $this->repository->loadDomainKeyPair($domain) - ), - $certificate - ); - - $this->actionHandler->handle($response); - - return; - } - - $monitoringLogger->debug('Certificate needs renewal', [ - 'domain' => $domain, - 'valid_until' => $parsedCertificate->getValidTo()->format('Y-m-d H:i:s'), - ]); - - $this->notice(sprintf( - 'Current certificate will expire in less than a week (%s), renewal is required.', - $parsedCertificate->getValidTo()->format('Y-m-d H:i:s')) - ); - } else { - $this->notice('Forced renewal.'); - } - - // Key pair - $this->info('Loading domain key pair...'); - $domainKeyPair = $this->repository->loadDomainKeyPair($domain); - - // Distinguished name - $this->info('Loading domain distinguished name...'); - $distinguishedName = $this->getOrCreateDistinguishedName($domain, $alternativeNames); - - // Order - $domains = array_merge([$domain], $alternativeNames); - $this->notice(sprintf('Loading the order related to the domains %s ...', implode(', ', $domains))); - if (!$this->getRepository()->hasCertificateOrder($domains)) { - throw new CommandFlowException('ask a challenge', 'authorize', $domains); - } - $order = $this->getRepository()->loadCertificateOrder($domains); - - // Renewal - $this->info(sprintf('Renewing certificate for domain %s ...', $domain)); - $csr = new CertificateRequest($distinguishedName, $domainKeyPair); - $response = $this->client->finalizeOrder($order, $csr); - $this->debug('Certificate received', ['certificate' => $response->getCertificate()->getPEM()]); - - $this->repository->storeDomainCertificate($domain, $response->getCertificate()); - $this->debug('Certificate stored'); - - // Post-generate actions - $this->info('Running post-generate actions...'); - $this->actionHandler->handle($response); - - $this->notice('Certificate renewed successfully!'); - - $monitoringLogger->info('Certificate renewed successfully', ['domain' => $domain]); - } catch (\Exception $e) { - $monitoringLogger->alert('A critical error occured during certificate renewal', ['exception' => $e]); - - throw $e; - } catch (\Throwable $e) { - $monitoringLogger->alert('A critical error occured during certificate renewal', ['exception' => $e]); - - throw $e; - } - - return 0; - } - - /** - * Retrieve the stored distinguishedName or create a new one if needed. - * - * @param string $domain - * - * @return DistinguishedName - */ - private function getOrCreateDistinguishedName($domain, array $alternativeNames) - { - if ($this->repository->hasDomainDistinguishedName($domain)) { - $original = $this->repository->loadDomainDistinguishedName($domain); - - $distinguishedName = new DistinguishedName( - $domain, - $this->input->getOption('country') ?: $original->getCountryName(), - $this->input->getOption('province') ?: $original->getStateOrProvinceName(), - $this->input->getOption('locality') ?: $original->getLocalityName(), - $this->input->getOption('organization') ?: $original->getOrganizationName(), - $this->input->getOption('unit') ?: $original->getOrganizationalUnitName(), - $this->input->getOption('email') ?: $original->getEmailAddress(), - $alternativeNames - ); - } else { - // Ask DistinguishedName - $distinguishedName = new DistinguishedName( - $domain, - $this->input->getOption('country'), - $this->input->getOption('province'), - $this->input->getOption('locality'), - $this->input->getOption('organization'), - $this->input->getOption('unit'), - $this->input->getOption('email'), - $alternativeNames - ); - - /** @var DistinguishedNameHelper $helper */ - $helper = $this->getHelper('distinguished_name'); - - if (!$helper->isReadyForRequest($distinguishedName)) { - $this->info("\n\nSome informations about you or your company are required for the certificate:\n"); - - $distinguishedName = $helper->ask( - $this->getHelper('question'), - $this->input, - $this->output, - $distinguishedName - ); - } - } - - $this->repository->storeDomainDistinguishedName($domain, $distinguishedName); - - return $distinguishedName; - } -} diff --git a/src/Cli/Command/RevokeCommand.php b/src/Cli/Command/RevokeCommand.php index 0a6db097..c6e0fa02 100644 --- a/src/Cli/Command/RevokeCommand.php +++ b/src/Cli/Command/RevokeCommand.php @@ -11,6 +11,7 @@ namespace AcmePhp\Cli\Command; +use AcmePhp\Cli\Application; use AcmePhp\Core\Exception\Protocol\CertificateRevocationException; use AcmePhp\Core\Protocol\RevocationReason; use Symfony\Component\Console\Input\InputArgument; @@ -20,10 +21,7 @@ class RevokeCommand extends AbstractCommand { - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $reasons = implode(PHP_EOL, RevocationReason::getFormattedReasons()); @@ -31,18 +29,26 @@ protected function configure() ->setDefinition([ new InputArgument('domain', InputArgument::REQUIRED, 'The domain revoke a certificate for'), new InputArgument('reason-code', InputOption::VALUE_OPTIONAL, 'The reason code for revocation:'.PHP_EOL.$reasons), + new InputOption( + 'provider', + null, + InputOption::VALUE_REQUIRED, + 'Certificate provider to use (supported: '.implode(', ', Application::PROVIDERS).')', + 'letsencrypt' + ), ]) ->setDescription('Revoke a SSL certificate for a domain') ->setHelp('The %command.name% command revoke a previously obtained certificate for a given domain'); } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { + if (!isset(Application::PROVIDERS[$this->input->getOption('provider')])) { + throw new \InvalidArgumentException('Invalid provider, supported: '.implode(', ', Application::PROVIDERS)); + } + $repository = $this->getRepository(); - $client = $this->getClient(); + $client = $this->getClient(Application::PROVIDERS[$this->input->getOption('provider')]); $domain = (string) $input->getArgument('domain'); $reasonCode = $input->getArgument('reason-code'); // ok to be null. LE expects 0 as default reason @@ -52,13 +58,13 @@ protected function execute(InputInterface $input, OutputInterface $output) } catch (\InvalidArgumentException $e) { $this->error('Reason code must be one of: '.PHP_EOL.implode(PHP_EOL, RevocationReason::getFormattedReasons())); - return; + return 1; } if (!$repository->hasDomainCertificate($domain)) { $this->error('Certificate for '.$domain.' not found locally'); - return; + return 1; } $certificate = $repository->loadDomainCertificate($domain); @@ -68,7 +74,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } catch (CertificateRevocationException $e) { $this->error($e->getMessage()); - return; + return 1; } $this->notice('Certificate revoked successfully!'); diff --git a/src/Cli/Command/RunCommand.php b/src/Cli/Command/RunCommand.php index 8204ee42..8bdd7b72 100644 --- a/src/Cli/Command/RunCommand.php +++ b/src/Cli/Command/RunCommand.php @@ -11,8 +11,10 @@ namespace AcmePhp\Cli\Command; +use AcmePhp\Cli\Application; use AcmePhp\Cli\Command\Helper\KeyOptionCommandTrait; use AcmePhp\Cli\Configuration\DomainConfiguration; +use AcmePhp\Cli\Exception\AcmeCliException; use AcmePhp\Core\Challenge\ConfigurableServiceInterface; use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface; use AcmePhp\Core\Challenge\SolverInterface; @@ -21,20 +23,23 @@ use AcmePhp\Core\Exception\Server\MalformedServerException; use AcmePhp\Core\Protocol\AuthorizationChallenge; use AcmePhp\Core\Protocol\CertificateOrder; +use AcmePhp\Core\Protocol\ExternalAccount; use AcmePhp\Ssl\CertificateRequest; use AcmePhp\Ssl\CertificateResponse; use AcmePhp\Ssl\DistinguishedName; use AcmePhp\Ssl\Generator\KeyOption; use AcmePhp\Ssl\KeyPair; use AcmePhp\Ssl\ParsedCertificate; +use GuzzleHttp\Client; use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\DependencyInjection\ServiceLocator; use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Path; use Symfony\Component\Yaml\Yaml; -use Webmozart\PathUtil\Path; /** * @author Jérémy Derussé @@ -43,10 +48,9 @@ class RunCommand extends AbstractCommand { use KeyOptionCommandTrait; - /** - * {@inheritdoc} - */ - protected function configure() + private $config; + + protected function configure(): void { $this->setName('run') ->setDefinition( @@ -61,24 +65,23 @@ protected function configure() ), ] ) - ->setDescription('Automaticaly chalenge domain and request certificates configured in the given file') - ->setHelp( - <<<'EOF' - The %command.name% challenge the domains, request the certificates and install them following a given configuration. -EOF - ); + ->setDescription('Automatically challenge domains and request certificates configured in the given file') + ->setHelp('The %command.name% challenge the domains, request the certificates and install them following a given configuration.'); } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - $config = $this->getConfig(Path::makeAbsolute($input->getArgument('config'), getcwd())); - $keyOption = $this->createKeyOption($config['key_type']); + $cwd = getcwd(); + if (false === $cwd) { + throw new \RuntimeException('Failed to get current working directory'); + } - $this->register($config['contact_email'], $keyOption); - foreach ($config['certificates'] as $domainConfig) { + $this->config = $this->getConfig(Path::makeAbsolute($input->getArgument('config'), $cwd)); + + $keyOption = $this->createKeyOption($this->config['key_type']); + $this->register($this->config['contact_email'], $keyOption); + + foreach ($this->config['certificates'] as $domainConfig) { $domain = $domainConfig['domain']; if ($this->isUpToDate($domain, $domainConfig, (int) $input->getOption('delay'))) { @@ -125,17 +128,62 @@ private function register($email, KeyOption $keyOption) $repository->storeAccountKeyPair($accountKeyPair); } - $client = $this->getClient(); + $client = $this->getClient(Application::PROVIDERS[$this->config['provider']]); $this->output->writeln('Registering on the ACME server...'); try { - $client->registerAccount(null, $email); + $client->registerAccount($email, $this->resolveEabKid()); $this->output->writeln('Account registered successfully!'); } catch (MalformedServerException $e) { $this->output->writeln('Account already registered!'); } } + private function resolveEabKid(): ?ExternalAccount + { + // If an External Account is provided, use it + if ($this->config['eab_kid'] && $this->config['eab_hmac_key']) { + return new ExternalAccount($this->config['eab_kid'], $this->config['eab_hmac_key']); + } + + // If using ZeroSSL ... + if ('zerossl' === $this->config['provider']) { + // If an API key is provided, use it + if ($this->config['zerossl_api_key']) { + $eabCredentials = json_decode( + (new Client()) + ->post('https://api.zerossl.com/acme/eab-credentials/?access_key='.$this->config['zerossl_api_key']) + ->getBody() + ->getContents() + ); + + if (!isset($eabCredentials->success) || !$eabCredentials->success) { + throw new AcmeCliException('ZeroSSL External account Binding failed: are you sure your API key is valid?'); + } + + return new ExternalAccount($eabCredentials->eab_kid, $eabCredentials->eab_hmac_key); + } + + // Otherwise register on the fly + $eabCredentials = json_decode( + (new Client()) + ->post('https://api.zerossl.com/acme/eab-credentials-email', [ + 'form_params' => ['email' => $this->config['contact_email']], + ]) + ->getBody() + ->getContents() + ); + + if (!isset($eabCredentials->success) || !$eabCredentials->success) { + throw new AcmeCliException('ZeroSSL External account Binding failed: registering your email failed.'); + } + + return new ExternalAccount($eabCredentials->eab_kid, $eabCredentials->eab_hmac_key); + } + + return null; + } + private function installCertificate(CertificateResponse $response, array $actions) { $this->output->writeln( @@ -199,7 +247,7 @@ private function requestCertificate(CertificateOrder $order, $domainConfig, KeyO $this->output->writeln(sprintf('Requesting certificate for domain %s...', $domain)); $repository = $this->getRepository(); - $client = $this->getClient(); + $client = $this->getClient(Application::PROVIDERS[$this->config['provider']]); $distinguishedName = new DistinguishedName( $domainConfig['domain'], $domainConfig['distinguished_name']['country'], @@ -235,7 +283,9 @@ private function challengeDomains(array $domainConfig) $solverConfig = $domainConfig['solver']; $domain = $domainConfig['domain']; + /** @var ServiceLocator $solverLocator */ $solverLocator = $this->getContainer()->get('acmephp.challenge_solver.locator'); + /** @var SolverInterface $solver */ $solver = $solverLocator->get($solverConfig['name']); if ($solver instanceof ConfigurableServiceInterface) { @@ -245,7 +295,7 @@ private function challengeDomains(array $domainConfig) /** @var ValidatorInterface $validator */ $validator = $this->getContainer()->get('challenge_validator'); - $client = $this->getClient(); + $client = $this->getClient(Application::PROVIDERS[$this->config['provider']]); $domains = array_unique(array_merge([$domain], $domainConfig['subject_alternative_names'])); $this->output->writeln('Requesting certificate order...'); @@ -289,7 +339,7 @@ private function challengeDomains(array $domainConfig) } $this->output->writeln(sprintf('Testing the challenge for domain %s...', $domain)); - if (time() - $startTestTime > 180 || !$validator->isValid($authorizationChallenge)) { + if (time() - $startTestTime > 180 || !$validator->isValid($authorizationChallenge, $solver)) { $this->output->writeln(sprintf('Can not self validate challenge for domain %s. Maybe letsencrypt will be able to do it...', $domain)); } @@ -313,9 +363,7 @@ private function challengeDomains(array $domainConfig) private function getConfig($configFile) { - return $this->resolveConfig( - $this->loadConfig($configFile) - ); + return $this->resolveConfig($this->loadConfig($configFile)); } private function loadConfig($configFile) diff --git a/src/Cli/Command/SelfUpdateCommand.php b/src/Cli/Command/SelfUpdateCommand.php deleted file mode 100644 index bb11a37f..00000000 --- a/src/Cli/Command/SelfUpdateCommand.php +++ /dev/null @@ -1,338 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Command; - -use Humbug\SelfUpdate\Strategy\GithubStrategy; -use Humbug\SelfUpdate\Strategy\ShaStrategy; -use Humbug\SelfUpdate\Updater; -use Humbug\SelfUpdate\VersionParser; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; - -/** - * Self-update command. - * - * Heavily inspired from https://github.com/padraic/phar-updater. - * - * @author Titouan Galopin - */ -class SelfUpdateCommand extends Command -{ - const PACKAGE_NAME = 'acmephp/acmephp'; - const FILE_NAME = 'acmephp.phar'; - const VERSION_URL = 'https://acmephp.github.io/downloads/acmephp.version'; - const PHAR_URL = 'https://acmephp.github.io/downloads/acmephp.phar'; - - /** - * @var string - */ - protected $version; - - /** - * @var OutputInterface - */ - protected $output; - - protected function configure() - { - $this - ->setName('self-update') - ->setDescription('Update acmephp.phar to most recent stable, pre-release or development build.') - ->addOption( - 'dev', - 'd', - InputOption::VALUE_NONE, - 'Update to most recent development build of Acme PHP.' - ) - ->addOption( - 'non-dev', - 'N', - InputOption::VALUE_NONE, - 'Update to most recent non-development (alpha/beta/stable) build of Acme PHP tagged on Github.' - ) - ->addOption( - 'pre', - 'p', - InputOption::VALUE_NONE, - 'Update to most recent pre-release version of Acme PHP (alpha/beta/rc) tagged on Github.' - ) - ->addOption( - 'stable', - 's', - InputOption::VALUE_NONE, - 'Update to most recent stable version tagged on Github.' - ) - ->addOption( - 'rollback', - 'r', - InputOption::VALUE_NONE, - 'Rollback to previous version of Acme PHP if available on filesystem.' - ) - ->addOption( - 'check', - 'c', - InputOption::VALUE_NONE, - 'Checks what updates are available across all possible stability tracks.' - ); - } - - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) - { - $this->output = $output; - $this->version = $this->getApplication()->getVersion(); - $parser = new VersionParser(); - - /* - * Check for ancilliary options - */ - if ($input->getOption('rollback')) { - $this->rollback(); - - return 0; - } - - if ($input->getOption('check')) { - $this->printAvailableUpdates(); - - return 0; - } - - /* - * Update to any specified stability option - */ - if ($input->getOption('dev')) { - $this->updateToDevelopmentBuild(); - - return 0; - } - - if ($input->getOption('pre')) { - $this->updateToPreReleaseBuild(); - - return 0; - } - - if ($input->getOption('stable')) { - $this->updateToStableBuild(); - - return 0; - } - - if ($input->getOption('non-dev')) { - $this->updateToMostRecentNonDevRemote(); - - return 0; - } - - /* - * If current build is stable, only update to more recent stable - * versions if available. User may specify otherwise using options. - */ - if ($parser->isStable($this->version)) { - $this->updateToStableBuild(); - - return 0; - } - - /* - * By default, update to most recent remote version regardless - * of stability. - */ - $this->updateToMostRecentNonDevRemote(); - - return 0; - } - - protected function getStableUpdater() - { - $updater = new Updater(); - $updater->setStrategy(Updater::STRATEGY_GITHUB); - - return $this->getGithubReleasesUpdater($updater); - } - - protected function getPreReleaseUpdater() - { - $updater = new Updater(); - $updater->setStrategy(Updater::STRATEGY_GITHUB); - $updater->getStrategy()->setStability(GithubStrategy::UNSTABLE); - - return $this->getGithubReleasesUpdater($updater); - } - - protected function getMostRecentNonDevUpdater() - { - $updater = new Updater(); - $updater->setStrategy(Updater::STRATEGY_GITHUB); - $updater->getStrategy()->setStability(GithubStrategy::ANY); - - return $this->getGithubReleasesUpdater($updater); - } - - protected function getGithubReleasesUpdater(Updater $updater) - { - $updater->getStrategy()->setPackageName(self::PACKAGE_NAME); - $updater->getStrategy()->setPharName(self::FILE_NAME); - $updater->getStrategy()->setCurrentLocalVersion($this->version); - - return $updater; - } - - protected function getDevelopmentUpdater() - { - $updater = new Updater(); - $updater->getStrategy()->setPharUrl(self::PHAR_URL); - $updater->getStrategy()->setVersionUrl(self::VERSION_URL); - - return $updater; - } - - protected function updateToStableBuild() - { - $this->update($this->getStableUpdater()); - } - - protected function updateToPreReleaseBuild() - { - $this->update($this->getPreReleaseUpdater()); - } - - protected function updateToMostRecentNonDevRemote() - { - $this->update($this->getMostRecentNonDevUpdater()); - } - - protected function updateToDevelopmentBuild() - { - $this->update($this->getDevelopmentUpdater()); - } - - protected function update(Updater $updater) - { - $this->output->writeln('Updating...'.PHP_EOL); - - try { - $result = $updater->update(); - - $newVersion = $updater->getNewVersion(); - $oldVersion = $updater->getOldVersion(); - if (40 === \strlen($newVersion)) { - $newVersion = 'dev-'.$newVersion; - } - if (40 === \strlen($oldVersion)) { - $oldVersion = 'dev-'.$oldVersion; - } - - if ($result) { - $this->output->writeln('Acme PHP has been updated.'); - $this->output->writeln(sprintf( - 'Current version is: %s.', - $newVersion - )); - $this->output->writeln(sprintf( - 'Previous version was: %s.', - $oldVersion - )); - } else { - $this->output->writeln('Acme PHP is currently up to date.'); - $this->output->writeln(sprintf( - 'Current version is: %s.', - $oldVersion - )); - } - } catch (\Exception $e) { - $this->output->writeln(sprintf('Error: %s', $e->getMessage())); - } - $this->output->write(PHP_EOL); - $this->output->writeln('You can also select update stability using --dev, --pre (alpha/beta/rc) or --stable.'); - } - - protected function rollback() - { - $updater = new Updater(); - - try { - $result = $updater->rollback(); - if ($result) { - $this->output->writeln('Acme PHP has been rolled back to prior version.'); - } else { - $this->output->writeln('Rollback failed for reasons unknown.'); - } - } catch (\Exception $e) { - $this->output->writeln(sprintf('Error: %s', $e->getMessage())); - } - } - - protected function printAvailableUpdates() - { - $this->printCurrentLocalVersion(); - $this->printCurrentStableVersion(); - $this->printCurrentPreReleaseVersion(); - $this->printCurrentDevVersion(); - $this->output->writeln('You can select update stability using --dev, --pre or --stable when self-updating.'); - } - - protected function printCurrentLocalVersion() - { - $this->output->writeln(sprintf( - 'Your current local build version is: %s', - $this->version - )); - } - - protected function printCurrentStableVersion() - { - $this->printVersion($this->getStableUpdater()); - } - - protected function printCurrentPreReleaseVersion() - { - $this->printVersion($this->getPreReleaseUpdater()); - } - - protected function printCurrentDevVersion() - { - $this->printVersion($this->getDevelopmentUpdater()); - } - - protected function printVersion(Updater $updater) - { - $stability = 'stable'; - if ($updater->getStrategy() instanceof ShaStrategy) { - $stability = 'development'; - } elseif ($updater->getStrategy() instanceof GithubStrategy - && GithubStrategy::UNSTABLE === $updater->getStrategy()->getStability()) { - $stability = 'pre-release'; - } - - try { - if ($updater->hasUpdate()) { - $this->output->writeln(sprintf( - 'The current %s build available remotely is: %s', - $stability, - $updater->getNewVersion() - )); - } elseif (false === $updater->getNewVersion()) { - $this->output->writeln(sprintf('There are no %s builds available.', $stability)); - } else { - $this->output->writeln(sprintf('You have the current %s build installed.', $stability)); - } - } catch (\Exception $e) { - $this->output->writeln(sprintf('Error: %s', $e->getMessage())); - } - } -} diff --git a/src/Cli/Command/StatusCommand.php b/src/Cli/Command/StatusCommand.php index daec4282..a1cfbae7 100644 --- a/src/Cli/Command/StatusCommand.php +++ b/src/Cli/Command/StatusCommand.php @@ -12,7 +12,7 @@ namespace AcmePhp\Cli\Command; use AcmePhp\Ssl\Parser\CertificateParser; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemOperator; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -23,10 +23,7 @@ */ class StatusCommand extends AbstractCommand { - /** - * {@inheritdoc} - */ - protected function configure() + protected function configure(): void { $this->setName('status') ->setDescription('List all the certificates handled by Acme PHP') @@ -38,15 +35,12 @@ protected function configure() ); } - /** - * {@inheritdoc} - */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $repository = $this->getRepository(); - /** @var FilesystemInterface $master */ - $master = $this->getContainer()->get('repository.master_storage'); + /** @var FilesystemOperator $master */ + $master = $this->getContainer()->get('repository.storage'); /** @var CertificateParser $certificateParser */ $certificateParser = $this->getContainer()->get('ssl.certificate_parser'); diff --git a/src/Cli/Configuration/AcmeConfiguration.php b/src/Cli/Configuration/AcmeConfiguration.php deleted file mode 100644 index ea15ff2b..00000000 --- a/src/Cli/Configuration/AcmeConfiguration.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Configuration; - -use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; -use Symfony\Component\Config\Definition\Builder\TreeBuilder; -use Symfony\Component\Config\Definition\ConfigurationInterface; - -/** - * @author Titouan Galopin - */ -class AcmeConfiguration implements ConfigurationInterface -{ - /** - * {@inheritdoc} - */ - public function getConfigTreeBuilder() - { - $treeBuilder = new TreeBuilder('acmephp'); - if (method_exists(TreeBuilder::class, 'getRootNode')) { - $rootNode = $treeBuilder->getRootNode(); - } else { - $rootNode = $treeBuilder->root('acmephp'); - } - - $this->createRootNode($rootNode); - - return $treeBuilder; - } - - protected function createRootNode(ArrayNodeDefinition $rootNode) - { - $rootNode - ->addDefaultsIfNotSet() - ->children() - ->arrayNode('storage') - ->info('Configure here where and how you want to save your certificates and SSL keys.') - ->children() - ->booleanNode('enable_backup') - ->info('By default, Acme PHP will create a backup of every file before any modification. You can disable this mechanism here.') - ->isRequired() - ->defaultTrue() - ->end() - ->arrayNode('post_generate') - ->info('Actions to execute right after the generation of a file (key, CSR or certificate). Actions are executed in the order provided in configuration.') - ->normalizeKeys(false) - ->prototype('variable') - ->cannotBeEmpty() - ->validate() - ->ifTrue(function ($action) { - return !\array_key_exists('action', $action); - }) - ->thenInvalid('The "action" configuration key is required.') - ->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('monitoring') - ->info('Configure here a simple monitoring mechanism that will warn you if an error occurs during a CRON job.') - ->normalizeKeys(false) - ->prototype('variable') - ->cannotBeEmpty() - ->end() - ->end() - ->end(); - } -} diff --git a/src/Cli/Configuration/DomainConfiguration.php b/src/Cli/Configuration/DomainConfiguration.php index 717551ce..4add46eb 100644 --- a/src/Cli/Configuration/DomainConfiguration.php +++ b/src/Cli/Configuration/DomainConfiguration.php @@ -11,6 +11,7 @@ namespace AcmePhp\Cli\Configuration; +use AcmePhp\Cli\Application; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -19,10 +20,7 @@ */ class DomainConfiguration implements ConfigurationInterface { - /** - * {@inheritdoc} - */ - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('acmephp'); if (method_exists(TreeBuilder::class, 'getRootNode')) { @@ -56,6 +54,28 @@ public function getConfigTreeBuilder() ->thenInvalid('The email %s is not valid.') ->end() ->end() + ->scalarNode('provider') + ->info('Certificate provider to use (supported: '.implode(', ', Application::PROVIDERS).')') + ->defaultValue('letsencrypt') + ->validate() + ->ifTrue(function ($item) { + return !isset(Application::PROVIDERS[$item]); + }) + ->thenInvalid('The certificate provider %s is not valid (supported: '.implode(', ', Application::PROVIDERS).').') + ->end() + ->end() + ->scalarNode('eab_kid') + ->info('External Account Binding identifier (optional)') + ->defaultNull() + ->end() + ->scalarNode('eab_hmac_key') + ->info('External Account Binding HMAC key (optional)') + ->defaultNull() + ->end() + ->scalarNode('zerossl_api_key') + ->info('ZeroSSL API key to use if you already have one (one will be created automatically otherwise)') + ->defaultNull() + ->end() ->scalarNode('key_type') ->info('Type of private key (RSA or EC).') ->defaultValue('RSA') diff --git a/src/Cli/Exception/AcmeCliActionException.php b/src/Cli/Exception/AcmeCliActionException.php index a18f3e6f..2275e275 100644 --- a/src/Cli/Exception/AcmeCliActionException.php +++ b/src/Cli/Exception/AcmeCliActionException.php @@ -16,7 +16,7 @@ */ class AcmeCliActionException extends AcmeCliException { - public function __construct($actionName, \Exception $previous = null) + public function __construct(string $actionName, ?\Exception $previous = null) { parent::__construct(sprintf('An exception was thrown during action "%s"', $actionName), $previous); } diff --git a/src/Cli/Exception/AcmeCliException.php b/src/Cli/Exception/AcmeCliException.php index b5f4c0c7..321021a9 100644 --- a/src/Cli/Exception/AcmeCliException.php +++ b/src/Cli/Exception/AcmeCliException.php @@ -16,7 +16,7 @@ */ class AcmeCliException extends \RuntimeException { - public function __construct($message, \Exception $previous = null) + public function __construct($message, ?\Exception $previous = null) { parent::__construct($message, 0, $previous); } diff --git a/src/Cli/Exception/AcmeDnsResolutionException.php b/src/Cli/Exception/AcmeDnsResolutionException.php deleted file mode 100644 index 1dd14ead..00000000 --- a/src/Cli/Exception/AcmeDnsResolutionException.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Exception; - -@trigger_error(sprintf('The "%s" class is deprecated since version 1.1 and will be removed in 2.0., use %s instead.', AcmeDnsResolutionException::class, \AcmePhp\Core\Exception\AcmeDnsResolutionException::class), E_USER_DEPRECATED); - -/** - * @author Jérémy Derussé - */ -class AcmeDnsResolutionException extends AcmeCliException -{ - public function __construct($message, \Exception $previous = null) - { - parent::__construct(null === $message ? 'An exception was thrown during resolution of DNS' : $message, $previous); - } -} diff --git a/src/Cli/Exception/CommandFlowException.php b/src/Cli/Exception/CommandFlowException.php index 3f3b3cb9..387577e8 100644 --- a/src/Cli/Exception/CommandFlowException.php +++ b/src/Cli/Exception/CommandFlowException.php @@ -25,7 +25,7 @@ class CommandFlowException extends AcmeCliException * @param string $command Name of the command to run in order to fix the flow * @param array $arguments Optional list of missing arguments */ - public function __construct($missing, $command, array $arguments = [], \Exception $previous = null) + public function __construct(string $missing, string $command, array $arguments = [], ?\Exception $previous = null) { $this->missing = $missing; $this->command = $command; @@ -43,26 +43,17 @@ public function __construct($missing, $command, array $arguments = [], \Exceptio parent::__construct($message, $previous); } - /** - * @return string - */ - public function getMissing() + public function getMissing(): string { return $this->missing; } - /** - * @return string - */ - public function getCommand() + public function getCommand(): string { return $this->command; } - /** - * @return array - */ - public function getArguments() + public function getArguments(): array { return $this->arguments; } diff --git a/src/Cli/Monitoring/EmailHandlerBuilder.php b/src/Cli/Monitoring/EmailHandlerBuilder.php deleted file mode 100644 index dfd00d1d..00000000 --- a/src/Cli/Monitoring/EmailHandlerBuilder.php +++ /dev/null @@ -1,67 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Monitoring; - -use AcmePhp\Cli\Exception\AcmeCliException; -use Monolog\Handler\FingersCrossedHandler; -use Monolog\Handler\SwiftMailerHandler; -use Monolog\Logger; - -/** - * @author Titouan Galopin - */ -class EmailHandlerBuilder implements HandlerBuilderInterface -{ - private static $defaults = [ - 'from' => 'monitoring@acmephp.github.io', - 'subject' => 'An error occured during Acme PHP CRON renewal', - 'port' => 25, - 'username' => null, - 'password' => null, - 'encryption' => null, - 'level' => Logger::ERROR, - ]; - - /** - * {@inheritdoc} - */ - public function createHandler($config) - { - if (!isset($config['host'])) { - throw new AcmeCliException('The SMTP host (key "host") is required in the email monitoring alert handler.'); - } - - if (!isset($config['to'])) { - throw new AcmeCliException('The mail recipient (key "to") is required in the email monitoring alert handler.'); - } - - $config = array_merge(self::$defaults, $config); - - $transport = new \Swift_SmtpTransport($config['host'], $config['port'], $config['encryption']); - - if ($config['username']) { - $transport->setUsername($config['username']); - } - - if ($config['password']) { - $transport->setPassword($config['password']); - } - - $message = new \Swift_Message($config['subject']); - $message->setFrom($config['from']); - $message->setTo($config['to']); - - $handler = new SwiftMailerHandler(new \Swift_Mailer($transport), $message); - - return new FingersCrossedHandler($handler, $config['level']); - } -} diff --git a/src/Cli/Monitoring/HandlerBuilderInterface.php b/src/Cli/Monitoring/HandlerBuilderInterface.php deleted file mode 100644 index a47aa163..00000000 --- a/src/Cli/Monitoring/HandlerBuilderInterface.php +++ /dev/null @@ -1,29 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Monitoring; - -use Monolog\Handler\HandlerInterface; - -interface HandlerBuilderInterface -{ - const LEVEL_ERROR = 'error'; - const LEVEL_INFO = 'info'; - - /** - * Create a handler usable with Monolog given a configuration. - * - * @param array $config - * - * @return HandlerInterface - */ - public function createHandler($config); -} diff --git a/src/Cli/Monitoring/MonitoringLoggerFactory.php b/src/Cli/Monitoring/MonitoringLoggerFactory.php deleted file mode 100644 index a65c84b8..00000000 --- a/src/Cli/Monitoring/MonitoringLoggerFactory.php +++ /dev/null @@ -1,62 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Monitoring; - -use AcmePhp\Cli\Exception\AcmeCliException; -use Monolog\Logger; -use Psr\Container\ContainerInterface; -use Psr\Log\NullLogger; - -/** - * @author Titouan Galopin - */ -class MonitoringLoggerFactory -{ - private $monitoringLocator; - private $monitoringConfig; - - private static $levels = [ - 'info' => Logger::INFO, - 'error' => Logger::ERROR, - ]; - - public function __construct(ContainerInterface $monitoringLocator, array $monitoringConfig) - { - $this->monitoringLocator = $monitoringLocator; - $this->monitoringConfig = $monitoringConfig; - } - - public function createLogger() - { - if (!$this->monitoringConfig) { - return new NullLogger(); - } - - $logger = new Logger('acmephp'); - - foreach ($this->monitoringConfig as $name => $config) { - if (isset($config['level'])) { - if (!isset(self::$levels[$config['level']])) { - throw new AcmeCliException(sprintf('Monitoring handler level "%s" is not valid.', $config['level'])); - } - - $config['level'] = self::$levels[$config['level']]; - } else { - $config['level'] = null; - } - - $logger->pushHandler($this->monitoringLocator->get($name)->createHandler($config)); - } - - return $logger; - } -} diff --git a/src/Cli/Monitoring/SlackHandlerBuilder.php b/src/Cli/Monitoring/SlackHandlerBuilder.php deleted file mode 100644 index d02e1c11..00000000 --- a/src/Cli/Monitoring/SlackHandlerBuilder.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Monitoring; - -use AcmePhp\Cli\Exception\AcmeCliException; -use Monolog\Handler\FingersCrossedHandler; -use Monolog\Handler\SlackHandler; -use Monolog\Logger; - -/** - * @author Titouan Galopin - */ -class SlackHandlerBuilder implements HandlerBuilderInterface -{ - private static $defaults = [ - 'username' => 'Acme PHP', - 'level' => Logger::INFO, - ]; - - /** - * {@inheritdoc} - */ - public function createHandler($config) - { - if (!isset($config['token'])) { - throw new AcmeCliException('The Slack token (key "token") is required in the slack monitoring alert handler.'); - } - - if (!isset($config['channel'])) { - throw new AcmeCliException('The Slack channel (key "channel") is required in the slack monitoring alert handler.'); - } - - $config = array_merge(self::$defaults, $config); - - $handler = new SlackHandler( - $config['token'], - '#'.ltrim($config['channel'], '#'), - $config['username'], - true, - null, - Logger::DEBUG - ); - - return new FingersCrossedHandler($handler, $config['level']); - } -} diff --git a/src/Cli/Monolog/ConsoleFormatter.php b/src/Cli/Monolog/ConsoleFormatter.php index 21fda4ca..2605bcde 100644 --- a/src/Cli/Monolog/ConsoleFormatter.php +++ b/src/Cli/Monolog/ConsoleFormatter.php @@ -12,7 +12,8 @@ namespace AcmePhp\Cli\Monolog; use Monolog\Formatter\LineFormatter; -use Monolog\Logger; +use Monolog\Level; +use Monolog\LogRecord; /** * Formats incoming records for console output by coloring them depending on log level. @@ -25,35 +26,35 @@ */ class ConsoleFormatter extends LineFormatter { - const SIMPLE_FORMAT = "%start_tag%%message% %context% %extra%%end_tag%\n"; + public const string SIMPLE_FORMAT = "%extra.start_tag%%message% %context% %extra%%extra.end_tag%\n"; - /** - * {@inheritdoc} - */ public function __construct($format = null, $dateFormat = null, $allowInlineLineBreaks = false, $ignoreEmptyContextAndExtra = true) { parent::__construct($format, $dateFormat, $allowInlineLineBreaks, $ignoreEmptyContextAndExtra); } - /** - * {@inheritdoc} - */ - public function format(array $record) + public function format(LogRecord $record): string { - if ($record['level'] >= Logger::ERROR) { - $record['start_tag'] = ''; - $record['end_tag'] = ''; - } elseif ($record['level'] >= Logger::WARNING) { - $record['start_tag'] = ''; - $record['end_tag'] = ''; - } elseif ($record['level'] >= Logger::NOTICE) { - $record['start_tag'] = ''; - $record['end_tag'] = ''; + if (Level::Error->includes($record->level)) { + $start = ''; + $end = ''; + } elseif (Level::Warning->includes($record->level)) { + $start = ''; + $end = ''; + } elseif (Level::Notice->includes($record->level)) { + $start = ''; + $end = ''; } else { - $record['start_tag'] = ''; - $record['end_tag'] = ''; + $start = ''; + $end = ''; } + $record->extra = [ + ...$record->extra, + 'start_tag' => $start, + 'end_tag' => $end, + ]; + return parent::format($record); } } diff --git a/src/Cli/Monolog/ConsoleHandler.php b/src/Cli/Monolog/ConsoleHandler.php index ea364e90..b1938b6f 100644 --- a/src/Cli/Monolog/ConsoleHandler.php +++ b/src/Cli/Monolog/ConsoleHandler.php @@ -11,9 +11,10 @@ namespace AcmePhp\Cli\Monolog; +use Monolog\Formatter\FormatterInterface; use Monolog\Handler\AbstractProcessingHandler; -use Monolog\Logger; -use Symfony\Component\Console\Output\Output; +use Monolog\Level; +use Monolog\LogRecord; use Symfony\Component\Console\Output\OutputInterface; /** @@ -21,59 +22,45 @@ * * Extracted from Symfony Monolog bridge. * - * @see https://github.com/symfony/monolog-bridge/edit/master/Handler/ConsoleHandler.php + * @see https://github.com/symfony/monolog-bridge/blob/7.1/Handler/ConsoleHandler.php * * @author Tobias Schultze */ class ConsoleHandler extends AbstractProcessingHandler { /** - * @var OutputInterface|null + * @var array */ - private $output; - - /** - * @var array - */ - private $verbosityLevelMap = [ - OutputInterface::VERBOSITY_QUIET => Logger::ERROR, - OutputInterface::VERBOSITY_NORMAL => Logger::INFO, - OutputInterface::VERBOSITY_VERBOSE => Logger::INFO, - OutputInterface::VERBOSITY_VERY_VERBOSE => Logger::INFO, - OutputInterface::VERBOSITY_DEBUG => Logger::DEBUG, + private array $verbosityLevelMap = [ + OutputInterface::VERBOSITY_QUIET => Level::Error, + OutputInterface::VERBOSITY_NORMAL => Level::Info, + OutputInterface::VERBOSITY_VERBOSE => Level::Info, + OutputInterface::VERBOSITY_VERY_VERBOSE => Level::Info, + OutputInterface::VERBOSITY_DEBUG => Level::Info, ]; /** - * Constructor. - * * @param OutputInterface|null $output The console output to use (the handler remains disabled when passing null * until the output is set, e.g. by using console events) * @param bool $bubble Whether the messages that are handled can bubble up the stack * @param array $verbosityLevelMap Array that maps the OutputInterface verbosity to a minimum logging * level (leave empty to use the default mapping) */ - public function __construct(OutputInterface $output = null, $bubble = true, array $verbosityLevelMap = []) + public function __construct(private ?OutputInterface $output = null, bool $bubble = true, array $verbosityLevelMap = []) { - parent::__construct(Logger::DEBUG, $bubble); - $this->output = $output; + parent::__construct(Level::Debug, $bubble); - if ($verbosityLevelMap) { + if ([] !== $verbosityLevelMap) { $this->verbosityLevelMap = $verbosityLevelMap; } } - /** - * {@inheritdoc} - */ - public function isHandling(array $record) + public function isHandling(LogRecord $record): bool { return $this->updateLevel() && parent::isHandling($record); } - /** - * {@inheritdoc} - */ - public function handle(array $record) + public function handle(LogRecord $record): bool { // we have to update the logging level each time because the verbosity of the // console output might have changed in the meantime (it is not immutable) @@ -82,10 +69,8 @@ public function handle(array $record) /** * Sets the console output to use for printing logs. - * - * @param OutputInterface $output The console output to use */ - public function setOutput(OutputInterface $output) + public function setOutput(OutputInterface $output): void { $this->output = $output; } @@ -93,25 +78,19 @@ public function setOutput(OutputInterface $output) /** * Disables the output. */ - public function close() + public function close(): void { $this->output = null; parent::close(); } - /** - * {@inheritdoc} - */ - protected function write(array $record) + protected function write(LogRecord $record): void { - $this->output->write((string) $record['formatted']); + $this->output->write((string) $record->formatted); } - /** - * {@inheritdoc} - */ - protected function getDefaultFormatter() + protected function getDefaultFormatter(): FormatterInterface { $formatter = new ConsoleFormatter(); $formatter->allowInlineLineBreaks(); @@ -124,7 +103,7 @@ protected function getDefaultFormatter() * * @return bool Whether the handler is enabled and verbosity is not set to quiet */ - private function updateLevel() + private function updateLevel(): bool { if (null === $this->output) { return false; @@ -134,7 +113,7 @@ private function updateLevel() if (isset($this->verbosityLevelMap[$verbosity])) { $this->setLevel($this->verbosityLevelMap[$verbosity]); } else { - $this->setLevel(Logger::DEBUG); + $this->setLevel(Level::Debug); } return true; diff --git a/src/Cli/Repository/Repository.php b/src/Cli/Repository/Repository.php index bcbf3dbe..6bb6bfb3 100644 --- a/src/Cli/Repository/Repository.php +++ b/src/Cli/Repository/Repository.php @@ -21,28 +21,28 @@ use AcmePhp\Ssl\KeyPair; use AcmePhp\Ssl\PrivateKey; use AcmePhp\Ssl\PublicKey; -use League\Flysystem\FilesystemInterface; +use League\Flysystem\FilesystemOperator; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\SerializerInterface; /** * @author Titouan Galopin */ -class Repository implements RepositoryV2Interface +class Repository implements RepositoryInterface { - const PATH_ACCOUNT_KEY_PRIVATE = 'account/key.private.pem'; - const PATH_ACCOUNT_KEY_PUBLIC = 'account/key.public.pem'; + public const PATH_ACCOUNT_KEY_PRIVATE = 'account/key.private.pem'; + public const PATH_ACCOUNT_KEY_PUBLIC = 'account/key.public.pem'; - const PATH_DOMAIN_KEY_PUBLIC = 'certs/{domain}/private/key.public.pem'; - const PATH_DOMAIN_KEY_PRIVATE = 'certs/{domain}/private/key.private.pem'; - const PATH_DOMAIN_CERT_CERT = 'certs/{domain}/public/cert.pem'; - const PATH_DOMAIN_CERT_CHAIN = 'certs/{domain}/public/chain.pem'; - const PATH_DOMAIN_CERT_FULLCHAIN = 'certs/{domain}/public/fullchain.pem'; - const PATH_DOMAIN_CERT_COMBINED = 'certs/{domain}/private/combined.pem'; + public const PATH_DOMAIN_KEY_PUBLIC = 'certs/{domain}/private/key.public.pem'; + public const PATH_DOMAIN_KEY_PRIVATE = 'certs/{domain}/private/key.private.pem'; + public const PATH_DOMAIN_CERT_CERT = 'certs/{domain}/public/cert.pem'; + public const PATH_DOMAIN_CERT_CHAIN = 'certs/{domain}/public/chain.pem'; + public const PATH_DOMAIN_CERT_FULLCHAIN = 'certs/{domain}/public/fullchain.pem'; + public const PATH_DOMAIN_CERT_COMBINED = 'certs/{domain}/private/combined.pem'; - const PATH_CACHE_AUTHORIZATION_CHALLENGE = 'var/{domain}/authorization_challenge.json'; - const PATH_CACHE_DISTINGUISHED_NAME = 'var/{domain}/distinguished_name.json'; - const PATH_CACHE_CERTIFICATE_ORDER = 'var/{domains}/certificate_order.json'; + public const PATH_CACHE_AUTHORIZATION_CHALLENGE = 'var/{domain}/authorization_challenge.json'; + public const PATH_CACHE_DISTINGUISHED_NAME = 'var/{domain}/distinguished_name.json'; + public const PATH_CACHE_CERTIFICATE_ORDER = 'var/{domains}/certificate_order.json'; /** * @var SerializerInterface @@ -50,34 +50,16 @@ class Repository implements RepositoryV2Interface private $serializer; /** - * @var FilesystemInterface + * @var FilesystemOperator */ - private $master; + private $storage; - /** - * @var FilesystemInterface - */ - private $backup; - - /** - * @var bool - */ - private $enableBackup; - - /** - * @param bool $enableBackup - */ - public function __construct(SerializerInterface $serializer, FilesystemInterface $master, FilesystemInterface $backup, $enableBackup) + public function __construct(SerializerInterface $serializer, FilesystemOperator $storage) { $this->serializer = $serializer; - $this->master = $master; - $this->backup = $backup; - $this->enableBackup = $enableBackup; + $this->storage = $storage; } - /** - * {@inheritdoc} - */ public function storeCertificateResponse(CertificateResponse $certificateResponse) { $distinguishedName = $certificateResponse->getCertificateRequest()->getDistinguishedName(); @@ -88,9 +70,6 @@ public function storeCertificateResponse(CertificateResponse $certificateRespons $this->storeDomainCertificate($domain, $certificateResponse->getCertificate()); } - /** - * {@inheritdoc} - */ public function storeAccountKeyPair(KeyPair $keyPair) { try { @@ -108,32 +87,16 @@ public function storeAccountKeyPair(KeyPair $keyPair) } } - private function getPathForDomain($path, $domain) + public function hasAccountKeyPair(): bool { - return strtr($path, ['{domain}' => $this->normalizeDomain($domain)]); + return $this->storage->has(self::PATH_ACCOUNT_KEY_PRIVATE); } - private function getPathForDomainList($path, array $domains) - { - return strtr($path, ['{domains}' => $this->normalizeDomainList($domains)]); - } - - /** - * {@inheritdoc} - */ - public function hasAccountKeyPair() - { - return $this->master->has(self::PATH_ACCOUNT_KEY_PRIVATE); - } - - /** - * {@inheritdoc} - */ - public function loadAccountKeyPair() + public function loadAccountKeyPair(): KeyPair { try { - $publicKeyPem = $this->master->read(self::PATH_ACCOUNT_KEY_PUBLIC); - $privateKeyPem = $this->master->read(self::PATH_ACCOUNT_KEY_PRIVATE); + $publicKeyPem = $this->storage->read(self::PATH_ACCOUNT_KEY_PUBLIC); + $privateKeyPem = $this->storage->read(self::PATH_ACCOUNT_KEY_PRIVATE); return new KeyPair( $this->serializer->deserialize($publicKeyPem, PublicKey::class, PemEncoder::FORMAT), @@ -144,10 +107,7 @@ public function loadAccountKeyPair() } } - /** - * {@inheritdoc} - */ - public function storeDomainKeyPair($domain, KeyPair $keyPair) + public function storeDomainKeyPair(string $domain, KeyPair $keyPair) { try { $this->save( @@ -164,22 +124,16 @@ public function storeDomainKeyPair($domain, KeyPair $keyPair) } } - /** - * {@inheritdoc} - */ - public function hasDomainKeyPair($domain) + public function hasDomainKeyPair(string $domain): bool { - return $this->master->has($this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain)); + return $this->storage->has($this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain)); } - /** - * {@inheritdoc} - */ - public function loadDomainKeyPair($domain) + public function loadDomainKeyPair(string $domain): KeyPair { try { - $publicKeyPem = $this->master->read($this->getPathForDomain(self::PATH_DOMAIN_KEY_PUBLIC, $domain)); - $privateKeyPem = $this->master->read($this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain)); + $publicKeyPem = $this->storage->read($this->getPathForDomain(self::PATH_DOMAIN_KEY_PUBLIC, $domain)); + $privateKeyPem = $this->storage->read($this->getPathForDomain(self::PATH_DOMAIN_KEY_PRIVATE, $domain)); return new KeyPair( $this->serializer->deserialize($publicKeyPem, PublicKey::class, PemEncoder::FORMAT), @@ -190,10 +144,7 @@ public function loadDomainKeyPair($domain) } } - /** - * {@inheritdoc} - */ - public function storeDomainAuthorizationChallenge($domain, AuthorizationChallenge $authorizationChallenge) + public function storeDomainAuthorizationChallenge(string $domain, AuthorizationChallenge $authorizationChallenge) { try { $this->save( @@ -205,21 +156,15 @@ public function storeDomainAuthorizationChallenge($domain, AuthorizationChalleng } } - /** - * {@inheritdoc} - */ - public function hasDomainAuthorizationChallenge($domain) + public function hasDomainAuthorizationChallenge(string $domain): bool { - return $this->master->has($this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain)); + return $this->storage->has($this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain)); } - /** - * {@inheritdoc} - */ - public function loadDomainAuthorizationChallenge($domain) + public function loadDomainAuthorizationChallenge(string $domain): AuthorizationChallenge { try { - $json = $this->master->read($this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain)); + $json = $this->storage->read($this->getPathForDomain(self::PATH_CACHE_AUTHORIZATION_CHALLENGE, $domain)); return $this->serializer->deserialize($json, AuthorizationChallenge::class, JsonEncoder::FORMAT); } catch (\Exception $e) { @@ -227,10 +172,7 @@ public function loadDomainAuthorizationChallenge($domain) } } - /** - * {@inheritdoc} - */ - public function storeDomainDistinguishedName($domain, DistinguishedName $distinguishedName) + public function storeDomainDistinguishedName(string $domain, DistinguishedName $distinguishedName) { try { $this->save( @@ -242,21 +184,15 @@ public function storeDomainDistinguishedName($domain, DistinguishedName $disting } } - /** - * {@inheritdoc} - */ - public function hasDomainDistinguishedName($domain) + public function hasDomainDistinguishedName(string $domain): bool { - return $this->master->has($this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain)); + return $this->storage->has($this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain)); } - /** - * {@inheritdoc} - */ - public function loadDomainDistinguishedName($domain) + public function loadDomainDistinguishedName(string $domain): DistinguishedName { try { - $json = $this->master->read($this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain)); + $json = $this->storage->read($this->getPathForDomain(self::PATH_CACHE_DISTINGUISHED_NAME, $domain)); return $this->serializer->deserialize($json, DistinguishedName::class, JsonEncoder::FORMAT); } catch (\Exception $e) { @@ -264,10 +200,7 @@ public function loadDomainDistinguishedName($domain) } } - /** - * {@inheritdoc} - */ - public function storeDomainCertificate($domain, Certificate $certificate) + public function storeDomainCertificate(string $domain, Certificate $certificate) { // Simple certificate $certPem = $this->serializer->serialize($certificate, PemEncoder::FORMAT); @@ -297,21 +230,15 @@ public function storeDomainCertificate($domain, Certificate $certificate) $this->save($this->getPathForDomain(self::PATH_DOMAIN_CERT_COMBINED, $domain), $combinedPem); } - /** - * {@inheritdoc} - */ - public function hasDomainCertificate($domain) + public function hasDomainCertificate(string $domain): bool { - return $this->master->has($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain)); + return $this->storage->has($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain)); } - /** - * {@inheritdoc} - */ - public function loadDomainCertificate($domain) + public function loadDomainCertificate(string $domain): Certificate { try { - $pems = explode('-----BEGIN CERTIFICATE-----', $this->master->read($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain))); + $pems = explode('-----BEGIN CERTIFICATE-----', $this->storage->read($this->getPathForDomain(self::PATH_DOMAIN_CERT_FULLCHAIN, $domain))); } catch (\Exception $e) { throw new AcmeCliException(sprintf('Loading of domain %s certificate failed', $domain), $e); } @@ -334,9 +261,6 @@ public function loadDomainCertificate($domain) return $certificate; } - /** - * {@inheritdoc} - */ public function storeCertificateOrder(array $domains, CertificateOrder $order) { try { @@ -349,21 +273,15 @@ public function storeCertificateOrder(array $domains, CertificateOrder $order) } } - /** - * {@inheritdoc} - */ - public function hasCertificateOrder(array $domains) + public function hasCertificateOrder(array $domains): bool { - return $this->master->has($this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains)); + return $this->storage->has($this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains)); } - /** - * {@inheritdoc} - */ - public function loadCertificateOrder(array $domains) + public function loadCertificateOrder(array $domains): CertificateOrder { try { - $json = $this->master->read($this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains)); + $json = $this->storage->read($this->getPathForDomainList(self::PATH_CACHE_CERTIFICATE_ORDER, $domains)); return $this->serializer->deserialize($json, CertificateOrder::class, JsonEncoder::FORMAT); } catch (\Exception $e) { @@ -371,59 +289,26 @@ public function loadCertificateOrder(array $domains) } } - /** - * {@inheritdoc} - */ - public function save($path, $content, $visibility = self::VISIBILITY_PRIVATE) + public function save(string $path, string $content, string $visibility = self::VISIBILITY_PRIVATE) { - if (!$this->master->has($path)) { - // File creation: remove from backup if it existed and warm-up both master and backup - $this->createAndBackup($path, $content); - } else { - // File update: backup before writing - $this->backupAndUpdate($path, $content); - } - - if ($this->enableBackup) { - $this->backup->setVisibility($path, $visibility); - } + $this->storage->write($path, $content); - $this->master->setVisibility($path, $visibility); + $this->storage->setVisibility($path, $visibility); } - private function createAndBackup($path, $content) + private function getPathForDomain($path, $domain) { - if ($this->enableBackup) { - if ($this->backup->has($path)) { - $this->backup->delete($path); - } - - $this->backup->write($path, $content); - } - - $this->master->write($path, $content); + return strtr($path, ['{domain}' => $this->normalizeDomain($domain)]); } - private function backupAndUpdate($path, $content) + private function getPathForDomainList($path, array $domains) { - if ($this->enableBackup) { - $oldContent = $this->master->read($path); - - if (false !== $oldContent) { - if ($this->backup->has($path)) { - $this->backup->update($path, $oldContent); - } else { - $this->backup->write($path, $oldContent); - } - } - } - - $this->master->update($path, $content); + return strtr($path, ['{domains}' => $this->normalizeDomainList($domains)]); } private function normalizeDomain($domain) { - return $domain; + return trim($domain); } private function normalizeDomainList(array $domains) diff --git a/src/Cli/Repository/RepositoryInterface.php b/src/Cli/Repository/RepositoryInterface.php index c36a285d..e1011cbe 100644 --- a/src/Cli/Repository/RepositoryInterface.php +++ b/src/Cli/Repository/RepositoryInterface.php @@ -13,6 +13,7 @@ use AcmePhp\Cli\Exception\AcmeCliException; use AcmePhp\Core\Protocol\AuthorizationChallenge; +use AcmePhp\Core\Protocol\CertificateOrder; use AcmePhp\Ssl\Certificate; use AcmePhp\Ssl\CertificateResponse; use AcmePhp\Ssl\DistinguishedName; @@ -23,8 +24,27 @@ */ interface RepositoryInterface { - const VISIBILITY_PUBLIC = 'public'; - const VISIBILITY_PRIVATE = 'private'; + public const VISIBILITY_PUBLIC = 'public'; + public const VISIBILITY_PRIVATE = 'private'; + + /** + * Store a given certificate as associated to a given domain. + * + * @throws AcmeCliException + */ + public function storeCertificateOrder(array $domains, CertificateOrder $order); + + /** + * Check if there is a certificate order associated to given domains in the repository. + */ + public function hasCertificateOrder(array $domains): bool; + + /** + * Load the certificate irder associated to given domains. + * + * @throws AcmeCliException + */ + public function loadCertificateOrder(array $domains): CertificateOrder; /** * Extract important elements from the given certificate response and store them @@ -49,142 +69,96 @@ public function storeAccountKeyPair(KeyPair $keyPair); /** * Check if there is an account key pair in the repository. - * - * @return bool */ - public function hasAccountKeyPair(); + public function hasAccountKeyPair(): bool; /** * Load the account key pair. * * @throws AcmeCliException - * - * @return KeyPair */ - public function loadAccountKeyPair(); + public function loadAccountKeyPair(): KeyPair; /** * Store a given key pair as associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException */ - public function storeDomainKeyPair($domain, KeyPair $keyPair); + public function storeDomainKeyPair(string $domain, KeyPair $keyPair); /** * Check if there is a key pair associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool */ - public function hasDomainKeyPair($domain); + public function hasDomainKeyPair(string $domain): bool; /** * Load the key pair associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException - * - * @return KeyPair */ - public function loadDomainKeyPair($domain); + public function loadDomainKeyPair(string $domain): KeyPair; /** * Store a given authorization challenge as associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException */ - public function storeDomainAuthorizationChallenge($domain, AuthorizationChallenge $authorizationChallenge); + public function storeDomainAuthorizationChallenge(string $domain, AuthorizationChallenge $authorizationChallenge); /** * Check if there is an authorization challenge associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool */ - public function hasDomainAuthorizationChallenge($domain); + public function hasDomainAuthorizationChallenge(string $domain): bool; /** * Load the authorization challenge associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException - * - * @return AuthorizationChallenge */ - public function loadDomainAuthorizationChallenge($domain); + public function loadDomainAuthorizationChallenge(string $domain): AuthorizationChallenge; /** * Store a given distinguished name as associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException */ - public function storeDomainDistinguishedName($domain, DistinguishedName $distinguishedName); + public function storeDomainDistinguishedName(string $domain, DistinguishedName $distinguishedName); /** * Check if there is a distinguished name associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool */ - public function hasDomainDistinguishedName($domain); + public function hasDomainDistinguishedName(string $domain): bool; /** * Load the distinguished name associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException - * - * @return DistinguishedName */ - public function loadDomainDistinguishedName($domain); + public function loadDomainDistinguishedName(string $domain): DistinguishedName; /** * Store a given certificate as associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException */ - public function storeDomainCertificate($domain, Certificate $certificate); + public function storeDomainCertificate(string $domain, Certificate $certificate); /** * Check if there is a certificate associated to the given domain in the repository. * - * @param string $domain - * * @return bool */ - public function hasDomainCertificate($domain); + public function hasDomainCertificate(string $domain); /** * Load the certificate associated to a given domain. * - * @param string $domain - * * @throws AcmeCliException - * - * @return Certificate */ - public function loadDomainCertificate($domain); + public function loadDomainCertificate(string $domain): Certificate; /** * Save a given string into a given path handling backup. - * - * @param string $path - * @param string $content - * @param string $visibility the visibilty to use for this file */ - public function save($path, $content, $visibility = self::VISIBILITY_PRIVATE); + public function save(string $path, string $content, string $visibility = self::VISIBILITY_PRIVATE); } diff --git a/src/Cli/Repository/RepositoryV2Interface.php b/src/Cli/Repository/RepositoryV2Interface.php deleted file mode 100644 index 26d0d9e9..00000000 --- a/src/Cli/Repository/RepositoryV2Interface.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Cli\Repository; - -use AcmePhp\Cli\Exception\AcmeCliException; -use AcmePhp\Core\Protocol\CertificateOrder; - -/** - * @author Titouan Galopin - */ -interface RepositoryV2Interface extends RepositoryInterface -{ - /** - * Store a given certificate as associated to a given domain. - * - * @throws AcmeCliException - */ - public function storeCertificateOrder(array $domains, CertificateOrder $order); - - /** - * Check if there is a certificate associated to the given domain in the repository. - * - * @param string $domain - * - * @return bool - */ - public function hasCertificateOrder(array $domains); - - /** - * Load the certificate associated to a given domain. - * - * @param string $domain - * - * @throws AcmeCliException - * - * @return CertificateOrder - */ - public function loadCertificateOrder(array $domains); -} diff --git a/src/Cli/Resources/services.xml b/src/Cli/Resources/services.xml index 0a511ea9..b518bc07 100644 --- a/src/Cli/Resources/services.xml +++ b/src/Cli/Resources/services.xml @@ -53,14 +53,14 @@ - + - + @@ -69,27 +69,17 @@ - + - + %app.storage_directory% - - - - %app.backup_directory% - - - - - - - %storage.enable_backup% + @@ -97,16 +87,8 @@ - - - - - - - - - + @@ -114,6 +96,10 @@ + + + + @@ -134,30 +120,6 @@ - - - - %storage.post_generate% - - - - - - - - - - - - - - - - - - %monitoring.handlers% - - @@ -194,6 +156,9 @@ + + + @@ -240,9 +205,11 @@ + + diff --git a/src/Cli/Serializer/PemEncoder.php b/src/Cli/Serializer/PemEncoder.php index 01d3a15d..94e52613 100644 --- a/src/Cli/Serializer/PemEncoder.php +++ b/src/Cli/Serializer/PemEncoder.php @@ -19,36 +19,24 @@ */ class PemEncoder implements EncoderInterface, DecoderInterface { - const FORMAT = 'pem'; + public const FORMAT = 'pem'; - /** - * {@inheritdoc} - */ - public function encode($data, $format, array $context = []) + public function encode(mixed $data, string $format, array $context = []): string { return trim($data)."\n"; } - /** - * {@inheritdoc} - */ - public function decode($data, $format, array $context = []) + public function decode(string $data, string $format, array $context = []): string { return trim($data)."\n"; } - /** - * {@inheritdoc} - */ - public function supportsEncoding($format) + public function supportsEncoding(string $format): bool { return self::FORMAT === $format; } - /** - * {@inheritdoc} - */ - public function supportsDecoding($format) + public function supportsDecoding(string $format): bool { return self::FORMAT === $format; } diff --git a/src/Cli/Serializer/PemNormalizer.php b/src/Cli/Serializer/PemNormalizer.php index 90104696..984c022f 100644 --- a/src/Cli/Serializer/PemNormalizer.php +++ b/src/Cli/Serializer/PemNormalizer.php @@ -21,35 +21,31 @@ */ class PemNormalizer implements NormalizerInterface, DenormalizerInterface { - /** - * {@inheritdoc} - */ - public function normalize($object, $format = null, array $context = []) + public function normalize(mixed $object, ?string $format = null, array $context = []): string { return $object->getPEM(); } - /** - * {@inheritdoc} - */ - public function denormalize($data, $class, $format = null, array $context = []) + public function denormalize(mixed $data, string $class, ?string $format = null, array $context = []): object { return new $class($data); } - /** - * {@inheritdoc} - */ - public function supportsNormalization($data, $format = null) + public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool { return \is_object($data) && ($data instanceof Certificate || $data instanceof Key); } - /** - * {@inheritdoc} - */ - public function supportsDenormalization($data, $type, $format = null) + public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool { return \is_string($data); } + + public function getSupportedTypes(?string $format): array + { + return [ + Certificate::class => true, + Key::class => true, + ]; + } } diff --git a/src/Core/AcmeClient.php b/src/Core/AcmeClient.php index 0b21e706..50ab20ac 100644 --- a/src/Core/AcmeClient.php +++ b/src/Core/AcmeClient.php @@ -21,12 +21,14 @@ use AcmePhp\Core\Http\SecureHttpClient; use AcmePhp\Core\Protocol\AuthorizationChallenge; use AcmePhp\Core\Protocol\CertificateOrder; +use AcmePhp\Core\Protocol\ExternalAccount; use AcmePhp\Core\Protocol\ResourcesDirectory; use AcmePhp\Core\Protocol\RevocationReason; use AcmePhp\Ssl\Certificate; use AcmePhp\Ssl\CertificateRequest; use AcmePhp\Ssl\CertificateResponse; use AcmePhp\Ssl\Signer\CertificateRequestSigner; +use GuzzleHttp\Psr7\Utils; use Webmozart\Assert\Assert; /** @@ -34,7 +36,7 @@ * * @author Titouan Galopin */ -class AcmeClient implements AcmeClientV2Interface +class AcmeClient implements AcmeClientInterface { /** * @var SecureHttpClient @@ -66,81 +68,49 @@ class AcmeClient implements AcmeClientV2Interface */ private $account; - /** - * @param string $directoryUrl - */ - public function __construct(SecureHttpClient $httpClient, $directoryUrl, CertificateRequestSigner $csrSigner = null) + public function __construct(SecureHttpClient $httpClient, string $directoryUrl, ?CertificateRequestSigner $csrSigner = null) { $this->uninitializedHttpClient = $httpClient; $this->directoryUrl = $directoryUrl; $this->csrSigner = $csrSigner ?: new CertificateRequestSigner(); } - /** - * {@inheritdoc} - */ - public function getHttpClient() - { - if (!$this->initializedHttpClient) { - $this->initializedHttpClient = $this->uninitializedHttpClient; - - $this->initializedHttpClient->setNonceEndpoint($this->getResourceUrl(ResourcesDirectory::NEW_NONCE)); - } - - return $this->initializedHttpClient; - } - - /** - * {@inheritdoc} - */ - public function registerAccount($agreement = null, $email = null) + public function registerAccount(?string $email = null, ?ExternalAccount $externalAccount = null): array { - Assert::nullOrString($agreement, 'registerAccount::$agreement expected a string or null. Got: %s'); - Assert::nullOrString($email, 'registerAccount::$email expected a string or null. Got: %s'); + $client = $this->getHttpClient(); $payload = [ 'termsOfServiceAgreed' => true, 'contact' => [], ]; - if (\is_string($email)) { + if ($email) { $payload['contact'][] = 'mailto:'.$email; } + if ($externalAccount) { + $payload['externalAccountBinding'] = $client->createExternalAccountPayload( + $externalAccount, + $this->getResourceUrl(ResourcesDirectory::NEW_ACCOUNT) + ); + } + $this->requestResource('POST', ResourcesDirectory::NEW_ACCOUNT, $payload); $account = $this->getResourceAccount(); - $client = $this->getHttpClient(); return $client->request('POST', $account, $client->signKidPayload($account, $account, null)); } - /** - * {@inheritdoc} - */ - public function requestAuthorization($domain) - { - $order = $this->requestOrder([$domain]); - - try { - return $order->getAuthorizationChallenges($domain); - } catch (AcmeCoreClientException $e) { - throw new ChallengeNotSupportedException(); - } - } - - /** - * {@inheritdoc} - */ - public function requestOrder(array $domains) + public function requestOrder(array $domains): CertificateOrder { Assert::allStringNotEmpty($domains, 'requestOrder::$domains expected a list of strings. Got: %s'); $payload = [ 'identifiers' => array_map( - function ($domain) { + static function ($domain) { return [ 'type' => 'dns', - 'value' => $domain, + 'value' => strtolower($domain), ]; }, array_values($domains) @@ -154,6 +124,7 @@ function ($domain) { throw new ChallengeNotSupportedException(); } + $authorizationsChallenges = []; $orderEndpoint = $client->getLastLocation(); foreach ($response['authorizations'] as $authorizationEndpoint) { $authorizationsResponse = $client->request('POST', $authorizationEndpoint, $client->signKidPayload($authorizationEndpoint, $this->getResourceAccount(), null)); @@ -163,109 +134,130 @@ function ($domain) { } } - return new CertificateOrder($authorizationsChallenges, $orderEndpoint); + return new CertificateOrder($authorizationsChallenges, $orderEndpoint, $response['status']); } - /** - * {@inheritdoc} - */ - public function reloadAuthorization(AuthorizationChallenge $challenge) + public function reloadOrder(CertificateOrder $order): CertificateOrder { $client = $this->getHttpClient(); - $challengeUrl = $challenge->getUrl(); - $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null)); + $orderEndpoint = $order->getOrderEndpoint(); + $response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null)); - return $this->createAuthorizationChallenge($challenge->getDomain(), $response); + if (!isset($response['authorizations']) || !$response['authorizations']) { + throw new ChallengeNotSupportedException(); + } + + $authorizationsChallenges = []; + foreach ($response['authorizations'] as $authorizationEndpoint) { + $authorizationsResponse = $client->request('POST', $authorizationEndpoint, $client->signKidPayload($authorizationEndpoint, $this->getResourceAccount(), null)); + $domain = (empty($authorizationsResponse['wildcard']) ? '' : '*.').$authorizationsResponse['identifier']['value']; + foreach ($authorizationsResponse['challenges'] as $challenge) { + $authorizationsChallenges[$domain][] = $this->createAuthorizationChallenge($authorizationsResponse['identifier']['value'], $challenge); + } + } + + return new CertificateOrder($authorizationsChallenges, $orderEndpoint, $response['status']); } - /** - * {@inheritdoc} - */ - public function challengeAuthorization(AuthorizationChallenge $challenge, $timeout = 180) + public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse { - Assert::integer($timeout, 'challengeAuthorization::$timeout expected an integer. Got: %s'); - $endTime = time() + $timeout; $client = $this->getHttpClient(); - $challengeUrl = $challenge->getUrl(); - $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null)); - if ('pending' === $response['status']) { - $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), [])); + $orderEndpoint = $order->getOrderEndpoint(); + $response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null)); + if (\in_array($response['status'], ['pending', 'processing', 'ready'])) { + $humanText = ['-----BEGIN CERTIFICATE REQUEST-----', '-----END CERTIFICATE REQUEST-----']; + + $csrContent = $this->csrSigner->signCertificateRequest($csr); + $csrContent = trim(str_replace($humanText, '', $csrContent)); + $csrContent = trim($client->getBase64Encoder()->encode(base64_decode($csrContent))); + + $response = $client->request('POST', $response['finalize'], $client->signKidPayload($response['finalize'], $this->getResourceAccount(), ['csr' => $csrContent])); } // Waiting loop - while (time() <= $endTime && (!isset($response['status']) || 'pending' === $response['status'])) { + while (time() <= $endTime && (!isset($response['status']) || \in_array($response['status'], ['pending', 'processing', 'ready']))) { sleep(1); - $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null)); + $response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null)); } - if (isset($response['status']) && 'pending' === $response['status']) { - throw new ChallengeTimedOutException($response); + if ('valid' !== $response['status']) { + throw new CertificateRequestFailedException('The order has not been validated'); } - if (!isset($response['status']) || 'valid' !== $response['status']) { - throw new ChallengeFailedException($response); + + $response = $client->rawRequest('POST', $response['certificate'], $client->signKidPayload($response['certificate'], $this->getResourceAccount(), null)); + $responseHeaders = $response->getHeaders(); + + if ($returnAlternateCertificateIfAvailable && isset($responseHeaders['Link'][1])) { + $matches = []; + preg_match('/<(http.*)>;rel="alternate"/', $responseHeaders['Link'][1], $matches); + + // If response headers include a valid alternate certificate link, return that certificate instead + if (isset($matches[1])) { + return $this->createCertificateResponse( + $csr, + $client->request('POST', $matches[1], $client->signKidPayload($matches[1], $this->getResourceAccount(), null), false) + ); + } } - return $response; + return $this->createCertificateResponse($csr, Utils::copyToString($response->getBody())); } - /** - * {@inheritdoc} - */ - public function requestCertificate($domain, CertificateRequest $csr, $timeout = 180) + public function requestAuthorization(string $domain): array { - Assert::stringNotEmpty($domain, 'requestCertificate::$domain expected a non-empty string. Got: %s'); - Assert::integer($timeout, 'requestCertificate::$timeout expected an integer. Got: %s'); - - $order = $this->requestOrder(array_unique(array_merge([$domain], $csr->getDistinguishedName()->getSubjectAlternativeNames()))); + $order = $this->requestOrder([$domain]); - return $this->finalizeOrder($order, $csr, $timeout); + try { + return $order->getAuthorizationChallenges($domain); + } catch (AcmeCoreClientException $e) { + throw new ChallengeNotSupportedException($e); + } } - /** - * {@inheritdoc} - */ - public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, $timeout = 180) + public function reloadAuthorization(AuthorizationChallenge $challenge): AuthorizationChallenge { - Assert::integer($timeout, 'finalizeOrder::$timeout expected an integer. Got: %s'); - - $endTime = time() + $timeout; $client = $this->getHttpClient(); - $orderEndpoint = $order->getOrderEndpoint(); - $response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null)); - if (\in_array($response['status'], ['pending', 'ready'])) { - $humanText = ['-----BEGIN CERTIFICATE REQUEST-----', '-----END CERTIFICATE REQUEST-----']; + $challengeUrl = $challenge->getUrl(); + $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null)); - $csrContent = $this->csrSigner->signCertificateRequest($csr); - $csrContent = trim(str_replace($humanText, '', $csrContent)); - $csrContent = trim($client->getBase64Encoder()->encode(base64_decode($csrContent))); + return $this->createAuthorizationChallenge($challenge->getDomain(), $response); + } - $response = $client->request('POST', $response['finalize'], $client->signKidPayload($response['finalize'], $this->getResourceAccount(), ['csr' => $csrContent])); + public function challengeAuthorization(AuthorizationChallenge $challenge, int $timeout = 180): array + { + $endTime = time() + $timeout; + $client = $this->getHttpClient(); + $challengeUrl = $challenge->getUrl(); + $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null)); + if ('pending' === $response['status'] || 'processing' === $response['status']) { + $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), [])); } // Waiting loop - while (time() <= $endTime && (!isset($response['status']) || \in_array($response['status'], ['pending', 'processing', 'ready']))) { + while (time() <= $endTime && (!isset($response['status']) || 'pending' === $response['status'] || 'processing' === $response['status'])) { sleep(1); - $response = $client->request('POST', $orderEndpoint, $client->signKidPayload($orderEndpoint, $this->getResourceAccount(), null)); + $response = (array) $client->request('POST', $challengeUrl, $client->signKidPayload($challengeUrl, $this->getResourceAccount(), null)); } - if ('valid' !== $response['status']) { - throw new CertificateRequestFailedException('The order has not been validated'); + if (isset($response['status']) && ('pending' === $response['status'] || 'processing' === $response['status'])) { + throw new ChallengeTimedOutException($response); } - - $response = $client->request('POST', $response['certificate'], $client->signKidPayload($response['certificate'], $this->getResourceAccount(), null), false); - $certificatesChain = null; - foreach (array_reverse(explode("\n\n", $response)) as $pem) { - $certificatesChain = new Certificate($pem, $certificatesChain); + if (!isset($response['status']) || 'valid' !== $response['status']) { + throw new ChallengeFailedException($response); } - return new CertificateResponse($csr, $certificatesChain); + return $response; } - /** - * {@inheritdoc} - */ - public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null) + public function requestCertificate(string $domain, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse + { + $order = $this->requestOrder(array_unique(array_merge([$domain], $csr->getDistinguishedName()->getSubjectAlternativeNames()))); + + return $this->finalizeOrder($order, $csr, $timeout, $returnAlternateCertificateIfAvailable); + } + + public function revokeCertificate(Certificate $certificate, ?RevocationReason $revocationReason = null) { if (!$endpoint = $this->getResourceUrl(ResourcesDirectory::REVOKE_CERT)) { throw new CertificateRevocationException('This ACME server does not support certificate revocation.'); @@ -297,13 +289,9 @@ public function revokeCertificate(Certificate $certificate, RevocationReason $re } /** - * Find a resource URL. - * - * @param string $resource - * - * @return string + * Find a resource URL from the Certificate Authority. */ - public function getResourceUrl($resource) + public function getResourceUrl(string $resource): string { if (!$this->directory) { $this->directory = new ResourcesDirectory( @@ -317,16 +305,12 @@ public function getResourceUrl($resource) /** * Request a resource (URL is found using ACME server directory). * - * @param string $method - * @param string $resource - * @param bool $returnJson + * @return array|string * * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code * @throws AcmeCoreClientException when an error occured during response parsing - * - * @return array|string */ - protected function requestResource($method, $resource, array $payload, $returnJson = true) + protected function requestResource(string $method, string $resource, array $payload, bool $returnJson = true) { $client = $this->getHttpClient(); $endpoint = $this->getResourceUrl($resource); @@ -339,12 +323,21 @@ protected function requestResource($method, $resource, array $payload, $returnJs ); } - /** - * Retrieve the resource account. - * - * @return string - */ - private function getResourceAccount() + private function createCertificateResponse(CertificateRequest $csr, string $certificate): CertificateResponse + { + $certificateHeader = '-----BEGIN CERTIFICATE-----'; + $certificatesChain = null; + + foreach (array_reverse(explode($certificateHeader, $certificate)) as $pem) { + if ('' !== \trim($pem)) { + $certificatesChain = new Certificate($certificateHeader.$pem, $certificatesChain); + } + } + + return new CertificateResponse($csr, $certificatesChain); + } + + private function getResourceAccount(): string { if (!$this->account) { $payload = [ @@ -358,7 +351,7 @@ private function getResourceAccount() return $this->account; } - private function createAuthorizationChallenge($domain, array $response) + private function createAuthorizationChallenge($domain, array $response): AuthorizationChallenge { $base64encoder = $this->getHttpClient()->getBase64Encoder(); @@ -371,4 +364,14 @@ private function createAuthorizationChallenge($domain, array $response) $response['token'].'.'.$base64encoder->encode($this->getHttpClient()->getJWKThumbprint()) ); } + + private function getHttpClient(): SecureHttpClient + { + if (!$this->initializedHttpClient) { + $this->initializedHttpClient = $this->uninitializedHttpClient; + $this->initializedHttpClient->setNonceEndpoint($this->getResourceUrl(ResourcesDirectory::NEW_NONCE)); + } + + return $this->initializedHttpClient; + } } diff --git a/src/Core/AcmeClientInterface.php b/src/Core/AcmeClientInterface.php index 148714d3..3ac4440d 100644 --- a/src/Core/AcmeClientInterface.php +++ b/src/Core/AcmeClientInterface.php @@ -19,8 +19,9 @@ use AcmePhp\Core\Exception\Protocol\ChallengeFailedException; use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; use AcmePhp\Core\Exception\Protocol\ChallengeTimedOutException; -use AcmePhp\Core\Http\SecureHttpClient; use AcmePhp\Core\Protocol\AuthorizationChallenge; +use AcmePhp\Core\Protocol\CertificateOrder; +use AcmePhp\Core\Protocol\ExternalAccount; use AcmePhp\Core\Protocol\RevocationReason; use AcmePhp\Ssl\Certificate; use AcmePhp\Ssl\CertificateRequest; @@ -36,16 +37,67 @@ interface AcmeClientInterface /** * Register the local account KeyPair in the Certificate Authority. * - * @param string|null $agreement an optionnal URI referring to a subscriber agreement or terms of service - * @param string|null $email an optionnal e-mail to associate with the account + * @param string|null $email an optionnal e-mail to associate with the account + * @param ExternalAccount|null $externalAccount an optionnal External Account to use for External Account Binding + * + * @return array the Certificate Authority response decoded from JSON into an array * * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code * (the exception will be more specific if detail is provided) * @throws AcmeCoreClientException when an error occured during response parsing + */ + public function registerAccount(?string $email = null, ?ExternalAccount $externalAccount = null): array; + + /** + * Request authorization challenge data for a list of domains. * - * @return array the Certificate Authority response decoded from JSON into an array + * An AuthorizationChallenge is an association between a URI, a token and a payload. + * The Certificate Authority will create this challenge data and you will then have + * to expose the payload for the verification (see challengeAuthorization). + * + * @param string[] $domains the domains to challenge + * + * @return CertificateOrder the Order returned by the Certificate Authority + * + * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code + * (the exception will be more specific if detail is provided) + * @throws AcmeCoreClientException when an error occured during response parsing + * @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server + */ + public function requestOrder(array $domains): CertificateOrder; + + /** + * Request the current status of a certificate order. */ - public function registerAccount($agreement = null, $email = null); + public function reloadOrder(CertificateOrder $order): CertificateOrder; + + /** + * Request a certificate for the given domain. + * + * This method should be called only if a previous authorization challenge has + * been successful for the asked domain. + * + * WARNING : This method SHOULD NOT BE USED in a web action. It will + * wait for the Certificate Authority to validate the certificate and + * this operation could be long. + * + * @param CertificateOrder $order the Order returned by the Certificate Authority + * @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate) + * @param int $timeout the timeout period + * @param bool $returnAlternateCertificateIfAvailable whether the alternate certificate provided by + * the CA should be returned instead of the main one. + * This is especially useful following + * https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html. + * + * @return CertificateResponse the certificate data to save it somewhere you want + * + * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code + * (the exception will be more specific if detail is provided) + * @throws AcmeCoreClientException when an error occured during response parsing + * @throws CertificateRequestFailedException when the certificate request failed + * @throws CertificateRequestTimedOutException when the certificate request timed out + */ + public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse; /** * Request authorization challenge data for a given domain. @@ -56,14 +108,23 @@ public function registerAccount($agreement = null, $email = null); * * @param string $domain the domain to challenge * + * @return AuthorizationChallenge[] the list of challenges data returned by the Certificate Authority + * * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code * (the exception will be more specific if detail is provided) * @throws AcmeCoreClientException when an error occured during response parsing * @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server + */ + public function requestAuthorization(string $domain): array; + + /** + * Request the current status of an authorization challenge. * - * @return AuthorizationChallenge[] the list of challenges data returned by the Certificate Authority + * @param AuthorizationChallenge $challenge The challenge to request + * + * @return AuthorizationChallenge A new instance of the challenge */ - public function requestAuthorization($domain); + public function reloadAuthorization(AuthorizationChallenge $challenge): AuthorizationChallenge; /** * Ask the Certificate Authority to challenge a given authorization. @@ -79,15 +140,15 @@ public function requestAuthorization($domain); * @param AuthorizationChallenge $challenge the challenge data to check * @param int $timeout the timeout period * + * @return array the validate challenge response + * * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code * (the exception will be more specific if detail is provided) * @throws AcmeCoreClientException when an error occured during response parsing * @throws ChallengeTimedOutException when the challenge timed out * @throws ChallengeFailedException when the challenge failed - * - * @return array the validate challenge response */ - public function challengeAuthorization(AuthorizationChallenge $challenge, $timeout = 180); + public function challengeAuthorization(AuthorizationChallenge $challenge, int $timeout = 180): array; /** * Request a certificate for the given domain. @@ -99,29 +160,28 @@ public function challengeAuthorization(AuthorizationChallenge $challenge, $timeo * wait for the Certificate Authority to validate the certificate and * this operation could be long. * - * @param string $domain the domain to request a certificate for - * @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate) - * @param int $timeout the timeout period + * @param string $domain the domain to request a certificate for + * @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate) + * @param int $timeout the timeout period + * @param bool $returnAlternateCertificateIfAvailable whether the alternate certificate provided by + * the CA should be returned instead of the main one. + * This is especially useful following + * https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html. + * + * @return CertificateResponse the certificate data to save it somewhere you want * * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code * (the exception will be more specific if detail is provided) * @throws AcmeCoreClientException when an error occured during response parsing * @throws CertificateRequestFailedException when the certificate request failed * @throws CertificateRequestTimedOutException when the certificate request timed out - * - * @return CertificateResponse the certificate data to save it somewhere you want */ - public function requestCertificate($domain, CertificateRequest $csr, $timeout = 180); + public function requestCertificate(string $domain, CertificateRequest $csr, int $timeout = 180, bool $returnAlternateCertificateIfAvailable = false): CertificateResponse; /** - * @throws CertificateRevocationException - */ - public function revokeCertificate(Certificate $certificate, RevocationReason $revocationReason = null); - - /** - * Get the HTTP client. + * Revoke a given certificate from the Certificate Authority. * - * @return SecureHttpClient + * @throws CertificateRevocationException */ - public function getHttpClient(); + public function revokeCertificate(Certificate $certificate, ?RevocationReason $revocationReason = null); } diff --git a/src/Core/AcmeClientV2Interface.php b/src/Core/AcmeClientV2Interface.php deleted file mode 100644 index c1f4f75d..00000000 --- a/src/Core/AcmeClientV2Interface.php +++ /dev/null @@ -1,81 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace AcmePhp\Core; - -use AcmePhp\Core\Exception\AcmeCoreClientException; -use AcmePhp\Core\Exception\AcmeCoreServerException; -use AcmePhp\Core\Exception\Protocol\CertificateRequestFailedException; -use AcmePhp\Core\Exception\Protocol\CertificateRequestTimedOutException; -use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException; -use AcmePhp\Core\Protocol\AuthorizationChallenge; -use AcmePhp\Core\Protocol\CertificateOrder; -use AcmePhp\Ssl\CertificateRequest; -use AcmePhp\Ssl\CertificateResponse; - -/** - * ACME protocol client interface. - * - * @author Titouan Galopin - */ -interface AcmeClientV2Interface extends AcmeClientInterface -{ - /** - * Request authorization challenge data for a list of domains. - * - * An AuthorizationChallenge is an association between a URI, a token and a payload. - * The Certificate Authority will create this challenge data and you will then have - * to expose the payload for the verification (see challengeAuthorization). - * - * @param string[] $domains the domains to challenge - * - * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code - * (the exception will be more specific if detail is provided) - * @throws AcmeCoreClientException when an error occured during response parsing - * @throws ChallengeNotSupportedException when the HTTP challenge is not supported by the server - * - * @return CertificateOrder the Order returned by the Certificate Authority - */ - public function requestOrder(array $domains); - - /** - * Request a certificate for the given domain. - * - * This method should be called only if a previous authorization challenge has - * been successful for the asked domain. - * - * WARNING : This method SHOULD NOT BE USED in a web action. It will - * wait for the Certificate Authority to validate the certificate and - * this operation could be long. - * - * @param CertificateOrder $order the Order returned by the Certificate Authority - * @param CertificateRequest $csr the Certificate Signing Request (informations for the certificate) - * @param int $timeout the timeout period - * - * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code - * (the exception will be more specific if detail is provided) - * @throws AcmeCoreClientException when an error occured during response parsing - * @throws CertificateRequestFailedException when the certificate request failed - * @throws CertificateRequestTimedOutException when the certificate request timed out - * - * @return CertificateResponse the certificate data to save it somewhere you want - */ - public function finalizeOrder(CertificateOrder $order, CertificateRequest $csr, $timeout = 180); - - /** - * Request the current status of an authorization challenge. - * - * @param AuthorizationChallenge $challenge The challenge to request - * - * @return AuthorizationChallenge A new instance of the challenge - */ - public function reloadAuthorization(AuthorizationChallenge $challenge); -} diff --git a/src/Core/Challenge/ChainValidator.php b/src/Core/Challenge/ChainValidator.php index a5a2d3a1..c9ec5481 100644 --- a/src/Core/Challenge/ChainValidator.php +++ b/src/Core/Challenge/ChainValidator.php @@ -21,9 +21,7 @@ */ class ChainValidator implements ValidatorInterface { - /** - * @var ValidatorInterface[] - */ + /** @var ValidatorInterface[] */ private $validators; /** @@ -34,13 +32,10 @@ public function __construct(array $validators) $this->validators = $validators; } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { foreach ($this->validators as $validator) { - if ($validator->supports($authorizationChallenge)) { + if ($validator->supports($authorizationChallenge, $solver)) { return true; } } @@ -48,14 +43,11 @@ public function supports(AuthorizationChallenge $authorizationChallenge) return false; } - /** - * {@inheritdoc} - */ - public function isValid(AuthorizationChallenge $authorizationChallenge) + public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { foreach ($this->validators as $validator) { - if ($validator->supports($authorizationChallenge)) { - return $validator->isValid($authorizationChallenge); + if ($validator->supports($authorizationChallenge, $solver)) { + return $validator->isValid($authorizationChallenge, $solver); } } diff --git a/src/Core/Challenge/Dns/DnsDataExtractor.php b/src/Core/Challenge/Dns/DnsDataExtractor.php index 00a7211f..135b774d 100644 --- a/src/Core/Challenge/Dns/DnsDataExtractor.php +++ b/src/Core/Challenge/Dns/DnsDataExtractor.php @@ -21,35 +21,26 @@ */ class DnsDataExtractor { - /** - * @var Base64SafeEncoder - */ + /** @var Base64SafeEncoder */ private $encoder; - /** - * @param Base64SafeEncoder $encoder - */ - public function __construct(Base64SafeEncoder $encoder = null) + public function __construct(?Base64SafeEncoder $encoder = null) { - $this->encoder = null === $encoder ? new Base64SafeEncoder() : $encoder; + $this->encoder = $encoder ?: new Base64SafeEncoder(); } /** * Retrieves the name of the TXT record to register. - * - * @return string */ - public function getRecordName(AuthorizationChallenge $authorizationChallenge) + public function getRecordName(AuthorizationChallenge $authorizationChallenge): string { return sprintf('_acme-challenge.%s.', $authorizationChallenge->getDomain()); } /** * Retrieves the value of the TXT record to register. - * - * @return string */ - public function getRecordValue(AuthorizationChallenge $authorizationChallenge) + public function getRecordValue(AuthorizationChallenge $authorizationChallenge): string { return $this->encoder->encode(hash('sha256', $authorizationChallenge->getPayload(), true)); } diff --git a/src/Core/Challenge/Dns/DnsResolverInterface.php b/src/Core/Challenge/Dns/DnsResolverInterface.php index b44bf94e..908e17f4 100644 --- a/src/Core/Challenge/Dns/DnsResolverInterface.php +++ b/src/Core/Challenge/Dns/DnsResolverInterface.php @@ -19,18 +19,12 @@ interface DnsResolverInterface { /** - * Retrieves the list of TXT entries for the given domain. - * - * @param string $domain - * - * @return array + * Return whether or not the Resolver is supported. */ - public function getTxtEntries($domain); + public static function isSupported(): bool; /** - * Return whether or not the Resolver is supported. - * - * @return bool + * Retrieves the list of TXT entries for the given domain. */ - public static function isSupported(); + public function getTxtEntries(string $domain): array; } diff --git a/src/Core/Challenge/Dns/DnsValidator.php b/src/Core/Challenge/Dns/DnsValidator.php index 56198bcd..bb6d9333 100644 --- a/src/Core/Challenge/Dns/DnsValidator.php +++ b/src/Core/Challenge/Dns/DnsValidator.php @@ -11,6 +11,7 @@ namespace AcmePhp\Core\Challenge\Dns; +use AcmePhp\Core\Challenge\SolverInterface; use AcmePhp\Core\Challenge\ValidatorInterface; use AcmePhp\Core\Exception\AcmeDnsResolutionException; use AcmePhp\Core\Protocol\AuthorizationChallenge; @@ -32,34 +33,28 @@ class DnsValidator implements ValidatorInterface */ private $dnsResolver; - /** - * @param DnsDataExtractor $extractor - * @param DnsResolverInterface $dnsResolver - */ - public function __construct(DnsDataExtractor $extractor = null, DnsResolverInterface $dnsResolver = null) + public function __construct(?DnsDataExtractor $extractor = null, ?DnsResolverInterface $dnsResolver = null) { - $this->extractor = null === $extractor ? new DnsDataExtractor() : $extractor; - $this->dnsResolver = null === $dnsResolver ? (LibDnsResolver::isSupported() ? new LibDnsResolver() : new SimpleDnsResolver()) : $dnsResolver; + $this->extractor = $extractor ?: new DnsDataExtractor(); + + $this->dnsResolver = $dnsResolver; + if (!$this->dnsResolver) { + $this->dnsResolver = LibDnsResolver::isSupported() ? new LibDnsResolver() : new SimpleDnsResolver(); + } } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { return 'dns-01' === $authorizationChallenge->getType(); } - /** - * {@inheritdoc} - */ - public function isValid(AuthorizationChallenge $authorizationChallenge) + public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { $recordName = $this->extractor->getRecordName($authorizationChallenge); $recordValue = $this->extractor->getRecordValue($authorizationChallenge); try { - return \in_array($recordValue, $this->dnsResolver->getTxtEntries($recordName)); + return \in_array($recordValue, $this->dnsResolver->getTxtEntries($recordName), false); } catch (AcmeDnsResolutionException $e) { return false; } diff --git a/src/Core/Challenge/Dns/GandiSolver.php b/src/Core/Challenge/Dns/GandiSolver.php index 28ec3bc2..1d5e39a4 100644 --- a/src/Core/Challenge/Dns/GandiSolver.php +++ b/src/Core/Challenge/Dns/GandiSolver.php @@ -28,6 +28,7 @@ class GandiSolver implements MultipleChallengesSolverInterface, ConfigurableServiceInterface { use LoggerAwareTrait; + /** * @var DnsDataExtractor */ @@ -48,16 +49,10 @@ class GandiSolver implements MultipleChallengesSolverInterface, ConfigurableServ */ private $apiKey; - /** - * @param DnsDataExtractor $extractor - * @param ClientInterface $client - */ - public function __construct( - DnsDataExtractor $extractor = null, - ClientInterface $client = null - ) { - $this->extractor = null === $extractor ? new DnsDataExtractor() : $extractor; - $this->client = null === $client ? new Client() : $client; + public function __construct(?DnsDataExtractor $extractor = null, ?ClientInterface $client = null) + { + $this->extractor = $extractor ?: new DnsDataExtractor(); + $this->client = $client ?: new Client(); $this->logger = new NullLogger(); } @@ -69,93 +64,93 @@ public function configure(array $config) $this->apiKey = $config['api_key']; } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge): bool { return 'dns-01' === $authorizationChallenge->getType(); } - /** - * {@inheritdoc} - */ public function solve(AuthorizationChallenge $authorizationChallenge) { return $this->solveAll([$authorizationChallenge]); } - /** - * {@inheritdoc} - */ public function solveAll(array $authorizationChallenges) { - Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class); - - foreach ($authorizationChallenges as $authorizationChallenge) { - $topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain()); - $recordName = $this->extractor->getRecordName($authorizationChallenge); - $recordValue = $this->extractor->getRecordValue($authorizationChallenge); - - $subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName); - - $this->client->request( - 'PUT', - 'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT', - [ - 'headers' => [ - 'X-Api-Key' => $this->apiKey, - ], - 'json' => [ - 'rrset_type' => 'TXT', - 'rrset_ttl' => 600, - 'rrset_name' => $subDomain, - 'rrset_values' => [$recordValue], - ], - ] - ); + $apiData = $this->prepareDataForAPICalls($authorizationChallenges); + + foreach ($apiData as $topLevelDomain => $subDomains) { + foreach ($subDomains as $subDomain => $recordValues) { + $this->client->request( + 'PUT', + 'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT', + [ + 'headers' => [ + 'X-Api-Key' => $this->apiKey, + ], + 'json' => [ + 'rrset_type' => 'TXT', + 'rrset_ttl' => 600, + 'rrset_name' => $subDomain, + 'rrset_values' => $recordValues, + ], + ] + ); + } } } - /** - * {@inheritdoc} - */ public function cleanup(AuthorizationChallenge $authorizationChallenge) { return $this->cleanupAll([$authorizationChallenge]); } - /** - * {@inheritdoc} - */ public function cleanupAll(array $authorizationChallenges) + { + $apiData = $this->prepareDataForAPICalls($authorizationChallenges); + + foreach ($apiData as $topLevelDomain => $subDomains) { + foreach (\array_keys($subDomains) as $subDomain) { + $this->client->request( + 'DELETE', + 'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT', + [ + 'headers' => [ + 'X-Api-Key' => $this->apiKey, + ], + ] + ); + } + } + } + + public function prepareDataForAPICalls(array $authorizationChallenges) { Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class); + $apiData = []; + foreach ($authorizationChallenges as $authorizationChallenge) { $topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain()); $recordName = $this->extractor->getRecordName($authorizationChallenge); + $recordValue = $this->extractor->getRecordValue($authorizationChallenge); $subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName); - $this->client->request( - 'DELETE', - 'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT', - [ - 'headers' => [ - 'X-Api-Key' => $this->apiKey, - ], - ] - ); + if (!\array_key_exists($topLevelDomain, $apiData)) { + $apiData[$topLevelDomain] = []; + } + + if (!\array_key_exists($subDomain, $apiData[$topLevelDomain])) { + $apiData[$topLevelDomain][$subDomain] = []; + } + + $apiData[$topLevelDomain][$subDomain][] = $recordValue; } + + return $apiData; } - /** - * @param string $domain - * - * @return string - */ - protected function getTopLevelDomain($domain) + protected function getTopLevelDomain(string $domain): string { return \implode('.', \array_slice(\explode('.', $domain), -2)); } diff --git a/src/Core/Challenge/Dns/LibDnsResolver.php b/src/Core/Challenge/Dns/LibDnsResolver.php index e3d8438b..2fa888b6 100644 --- a/src/Core/Challenge/Dns/LibDnsResolver.php +++ b/src/Core/Challenge/Dns/LibDnsResolver.php @@ -58,25 +58,24 @@ class LibDnsResolver implements DnsResolverInterface private $nameServer; public function __construct( - QuestionFactory $questionFactory = null, - MessageFactory $messageFactory = null, - Encoder $encoder = null, - Decoder $decoder = null, + ?QuestionFactory $questionFactory = null, + ?MessageFactory $messageFactory = null, + ?Encoder $encoder = null, + ?Decoder $decoder = null, $nameServer = '8.8.8.8' ) { - $this->questionFactory = null === $questionFactory ? new QuestionFactory() : $questionFactory; - $this->messageFactory = null === $messageFactory ? new MessageFactory() : $messageFactory; - $this->encoder = null === $encoder ? (new EncoderFactory())->create() : $encoder; - $this->decoder = null === $decoder ? (new DecoderFactory())->create() : $decoder; + $this->questionFactory = $questionFactory ?: new QuestionFactory(); + $this->messageFactory = $messageFactory ?: new MessageFactory(); + $this->encoder = $encoder ?: (new EncoderFactory())->create(); + $this->decoder = $decoder ?: (new DecoderFactory())->create(); $this->nameServer = $nameServer; - $this->logger = new NullLogger(); } /** * @{@inheritdoc} */ - public static function isSupported() + public static function isSupported(): bool { return class_exists(ResourceTypes::class); } @@ -84,7 +83,7 @@ public static function isSupported() /** * @{@inheritdoc} */ - public function getTxtEntries($domain) + public function getTxtEntries($domain): array { $domain = rtrim($domain, '.'); $nameServers = $this->getNameServers($domain); @@ -98,7 +97,7 @@ public function getTxtEntries($domain) try { $response = $this->request($domain, ResourceTypes::TXT, $ipNameServer[0]); } catch (\Exception $e) { - throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer)); + throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer), $e); } $entries = []; foreach ($response->getAnswerRecords() as $record) { diff --git a/src/Core/Challenge/Dns/Route53Solver.php b/src/Core/Challenge/Dns/Route53Solver.php index 9486715d..53975320 100644 --- a/src/Core/Challenge/Dns/Route53Solver.php +++ b/src/Core/Challenge/Dns/Route53Solver.php @@ -27,6 +27,7 @@ class Route53Solver implements MultipleChallengesSolverInterface { use LoggerAwareTrait; + /** * @var DnsDataExtractor */ @@ -42,38 +43,23 @@ class Route53Solver implements MultipleChallengesSolverInterface */ private $cacheZones; - /** - * @param DnsDataExtractor $extractor - * @param Route53Client $client - */ - public function __construct( - DnsDataExtractor $extractor = null, - Route53Client $client = null - ) { - $this->extractor = null === $extractor ? new DnsDataExtractor() : $extractor; - $this->client = null === $client ? new Route53Client([]) : $client; + public function __construct(?DnsDataExtractor $extractor = null, ?Route53Client $client = null) + { + $this->extractor = $extractor ?: new DnsDataExtractor(); + $this->client = $client ?: new Route53Client([]); $this->logger = new NullLogger(); } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge): bool { return 'dns-01' === $authorizationChallenge->getType(); } - /** - * {@inheritdoc} - */ public function solve(AuthorizationChallenge $authorizationChallenge) { return $this->solveAll([$authorizationChallenge]); } - /** - * {@inheritdoc} - */ public function solveAll(array $authorizationChallenges) { Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class); @@ -119,17 +105,11 @@ public function solveAll(array $authorizationChallenges) } } - /** - * {@inheritdoc} - */ public function cleanup(AuthorizationChallenge $authorizationChallenge) { return $this->cleanupAll([$authorizationChallenge]); } - /** - * {@inheritdoc} - */ public function cleanupAll(array $authorizationChallenges) { Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class); @@ -203,7 +183,7 @@ function ($recordSet) use ($recordName) { private function getSaveRecordQuery($recordName, array $recordIndex) { - //remove old indexes + // remove old indexes $limitTime = time() - 86400; foreach ($recordIndex as $recordValue => $time) { if ($time < $limitTime) { diff --git a/src/Core/Challenge/Dns/SimpleDnsResolver.php b/src/Core/Challenge/Dns/SimpleDnsResolver.php index f7161214..cee454f7 100644 --- a/src/Core/Challenge/Dns/SimpleDnsResolver.php +++ b/src/Core/Challenge/Dns/SimpleDnsResolver.php @@ -21,7 +21,7 @@ class SimpleDnsResolver implements DnsResolverInterface /** * @{@inheritdoc} */ - public static function isSupported() + public static function isSupported(): bool { return \function_exists('dns_get_record'); } @@ -29,7 +29,7 @@ public static function isSupported() /** * @{@inheritdoc} */ - public function getTxtEntries($domain) + public function getTxtEntries($domain): array { $entries = []; foreach (dns_get_record($domain, DNS_TXT) as $record) { diff --git a/src/Core/Challenge/Dns/SimpleDnsSolver.php b/src/Core/Challenge/Dns/SimpleDnsSolver.php index 25e2b981..f5703acd 100644 --- a/src/Core/Challenge/Dns/SimpleDnsSolver.php +++ b/src/Core/Challenge/Dns/SimpleDnsSolver.php @@ -33,27 +33,17 @@ class SimpleDnsSolver implements SolverInterface */ protected $output; - /** - * @param DnsDataExtractor $extractor - * @param OutputInterface $output - */ - public function __construct(DnsDataExtractor $extractor = null, OutputInterface $output = null) + public function __construct(?DnsDataExtractor $extractor = null, ?OutputInterface $output = null) { - $this->extractor = null === $extractor ? new DnsDataExtractor() : $extractor; - $this->output = null === $output ? new NullOutput() : $output; + $this->extractor = $extractor ?: new DnsDataExtractor(); + $this->output = $output ?: new NullOutput(); } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge): bool { return 'dns-01' === $authorizationChallenge->getType(); } - /** - * {@inheritdoc} - */ public function solve(AuthorizationChallenge $authorizationChallenge) { $recordName = $this->extractor->getRecordName($authorizationChallenge); @@ -80,9 +70,6 @@ public function solve(AuthorizationChallenge $authorizationChallenge) ); } - /** - * {@inheritdoc} - */ public function cleanup(AuthorizationChallenge $authorizationChallenge) { $recordName = $this->extractor->getRecordName($authorizationChallenge); diff --git a/src/Core/Challenge/Http/FilesystemSolver.php b/src/Core/Challenge/Http/FilesystemSolver.php index 72652f46..58a781e5 100644 --- a/src/Core/Challenge/Http/FilesystemSolver.php +++ b/src/Core/Challenge/Http/FilesystemSolver.php @@ -43,14 +43,10 @@ class FilesystemSolver implements SolverInterface, ConfigurableServiceInterface */ private $extractor; - /** - * @param ContainerInterface $filesystemFactoryLocator - * @param HttpDataExtractor $extractor - */ - public function __construct(ContainerInterface $filesystemFactoryLocator = null, HttpDataExtractor $extractor = null) + public function __construct(?ContainerInterface $filesystemFactoryLocator = null, ?HttpDataExtractor $extractor = null) { - $this->filesystemFactoryLocator = $filesystemFactoryLocator = null ? new ServiceLocator([]) : $filesystemFactoryLocator; - $this->extractor = null === $extractor ? new HttpDataExtractor() : $extractor; + $this->filesystemFactoryLocator = $filesystemFactoryLocator ?: new ServiceLocator([]); + $this->extractor = $extractor ?: new HttpDataExtractor(); $this->filesystem = new NullAdapter(); } @@ -63,17 +59,11 @@ public function configure(array $config) $this->filesystem = $factory->create($config); } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge): bool { return 'http-01' === $authorizationChallenge->getType(); } - /** - * {@inheritdoc} - */ public function solve(AuthorizationChallenge $authorizationChallenge) { $checkPath = $this->extractor->getCheckPath($authorizationChallenge); @@ -82,9 +72,6 @@ public function solve(AuthorizationChallenge $authorizationChallenge) $this->filesystem->write($checkPath, $checkContent); } - /** - * {@inheritdoc} - */ public function cleanup(AuthorizationChallenge $authorizationChallenge) { $checkPath = $this->extractor->getCheckPath($authorizationChallenge); diff --git a/src/Core/Challenge/Http/HttpDataExtractor.php b/src/Core/Challenge/Http/HttpDataExtractor.php index 21f97d3f..6e635505 100644 --- a/src/Core/Challenge/Http/HttpDataExtractor.php +++ b/src/Core/Challenge/Http/HttpDataExtractor.php @@ -22,10 +22,8 @@ class HttpDataExtractor { /** * Retrieves the absolute URL called by the CA. - * - * @return string */ - public function getCheckUrl(AuthorizationChallenge $authorizationChallenge) + public function getCheckUrl(AuthorizationChallenge $authorizationChallenge): string { return sprintf( 'http://%s%s', @@ -36,10 +34,8 @@ public function getCheckUrl(AuthorizationChallenge $authorizationChallenge) /** * Retrieves the absolute path called by the CA. - * - * @return string */ - public function getCheckPath(AuthorizationChallenge $authorizationChallenge) + public function getCheckPath(AuthorizationChallenge $authorizationChallenge): string { return sprintf( '/.well-known/acme-challenge/%s', @@ -49,10 +45,8 @@ public function getCheckPath(AuthorizationChallenge $authorizationChallenge) /** * Retrieves the content that should be returned in the response. - * - * @return string */ - public function getCheckContent(AuthorizationChallenge $authorizationChallenge) + public function getCheckContent(AuthorizationChallenge $authorizationChallenge): string { return $authorizationChallenge->getPayload(); } diff --git a/src/Core/Challenge/Http/HttpValidator.php b/src/Core/Challenge/Http/HttpValidator.php index 03d7b686..f0933fe6 100644 --- a/src/Core/Challenge/Http/HttpValidator.php +++ b/src/Core/Challenge/Http/HttpValidator.php @@ -11,6 +11,7 @@ namespace AcmePhp\Core\Challenge\Http; +use AcmePhp\Core\Challenge\SolverInterface; use AcmePhp\Core\Challenge\ValidatorInterface; use AcmePhp\Core\Protocol\AuthorizationChallenge; use GuzzleHttp\Client; @@ -33,30 +34,24 @@ class HttpValidator implements ValidatorInterface */ private $client; - public function __construct(HttpDataExtractor $extractor = null, Client $client = null) + public function __construct(?HttpDataExtractor $extractor = null, ?Client $client = null) { - $this->extractor = null === $extractor ? new HttpDataExtractor() : $extractor; - $this->client = null === $client ? new Client() : $client; + $this->extractor = $extractor ?: new HttpDataExtractor(); + $this->client = $client ?: new Client(); } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { - return 'http-01' === $authorizationChallenge->getType(); + return 'http-01' === $authorizationChallenge->getType() && !$solver instanceof MockServerHttpSolver; } - /** - * {@inheritdoc} - */ - public function isValid(AuthorizationChallenge $authorizationChallenge) + public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { $checkUrl = $this->extractor->getCheckUrl($authorizationChallenge); $checkContent = $this->extractor->getCheckContent($authorizationChallenge); try { - return $checkContent === trim($this->client->get($checkUrl)->getBody()->getContents()); + return $checkContent === trim($this->client->get($checkUrl, ['verify' => false])->getBody()->getContents()); } catch (ClientException $e) { return false; } diff --git a/src/Core/Challenge/Http/MockHttpValidator.php b/src/Core/Challenge/Http/MockHttpValidator.php new file mode 100644 index 00000000..15b2cbea --- /dev/null +++ b/src/Core/Challenge/Http/MockHttpValidator.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Challenge\Http; + +use AcmePhp\Core\Challenge\SolverInterface; +use AcmePhp\Core\Challenge\ValidatorInterface; +use AcmePhp\Core\Protocol\AuthorizationChallenge; + +/** + * Validator for pebble-challtestsrv. + * + * @author Titouan Galopin + */ +class MockHttpValidator implements ValidatorInterface +{ + public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool + { + return 'http-01' === $authorizationChallenge->getType() && $solver instanceof MockServerHttpSolver; + } + + public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool + { + return true; + } +} diff --git a/src/Core/Challenge/Http/MockServerHttpSolver.php b/src/Core/Challenge/Http/MockServerHttpSolver.php new file mode 100644 index 00000000..4ea416d3 --- /dev/null +++ b/src/Core/Challenge/Http/MockServerHttpSolver.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Challenge\Http; + +use AcmePhp\Core\Challenge\SolverInterface; +use AcmePhp\Core\Protocol\AuthorizationChallenge; +use GuzzleHttp\Client; +use GuzzleHttp\RequestOptions; + +/** + * ACME HTTP solver talking to pebble-challtestsrv. + * + * @author Titouan Galopin + */ +class MockServerHttpSolver implements SolverInterface +{ + public function supports(AuthorizationChallenge $authorizationChallenge): bool + { + return 'http-01' === $authorizationChallenge->getType(); + } + + public function solve(AuthorizationChallenge $authorizationChallenge) + { + (new Client())->post('http://localhost:8055/add-http01', [ + RequestOptions::JSON => [ + 'token' => $authorizationChallenge->getToken(), + 'content' => $authorizationChallenge->getPayload(), + ], + ]); + } + + public function cleanup(AuthorizationChallenge $authorizationChallenge) + { + (new Client())->post('http://localhost:8055/del-http01', [ + RequestOptions::JSON => [ + 'token' => $authorizationChallenge->getToken(), + ], + ]); + } +} diff --git a/src/Core/Challenge/Http/SimpleHttpSolver.php b/src/Core/Challenge/Http/SimpleHttpSolver.php index edc06058..b0dea71c 100644 --- a/src/Core/Challenge/Http/SimpleHttpSolver.php +++ b/src/Core/Challenge/Http/SimpleHttpSolver.php @@ -33,27 +33,17 @@ class SimpleHttpSolver implements SolverInterface */ private $output; - /** - * @param HttpDataExtractor $extractor - * @param OutputInterface $output - */ - public function __construct(HttpDataExtractor $extractor = null, OutputInterface $output = null) + public function __construct(?HttpDataExtractor $extractor = null, ?OutputInterface $output = null) { - $this->extractor = null === $extractor ? new HttpDataExtractor() : $extractor; - $this->output = null === $output ? new NullOutput() : $output; + $this->extractor = $extractor ?: new HttpDataExtractor(); + $this->output = $output ?: new NullOutput(); } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge): bool { return 'http-01' === $authorizationChallenge->getType(); } - /** - * {@inheritdoc} - */ public function solve(AuthorizationChallenge $authorizationChallenge) { $checkUrl = $this->extractor->getCheckUrl($authorizationChallenge); @@ -79,9 +69,6 @@ public function solve(AuthorizationChallenge $authorizationChallenge) ); } - /** - * {@inheritdoc} - */ public function cleanup(AuthorizationChallenge $authorizationChallenge) { $checkUrl = $this->extractor->getCheckUrl($authorizationChallenge); diff --git a/src/Core/Challenge/SolverInterface.php b/src/Core/Challenge/SolverInterface.php index 3551a05d..59ef4204 100644 --- a/src/Core/Challenge/SolverInterface.php +++ b/src/Core/Challenge/SolverInterface.php @@ -25,7 +25,7 @@ interface SolverInterface * * @return bool The solver supports the given challenge's type */ - public function supports(AuthorizationChallenge $authorizationChallenge); + public function supports(AuthorizationChallenge $authorizationChallenge): bool; /** * Solve the given authorization challenge. diff --git a/src/Core/Challenge/ValidatorInterface.php b/src/Core/Challenge/ValidatorInterface.php index 3aca9ad7..573e2d6a 100644 --- a/src/Core/Challenge/ValidatorInterface.php +++ b/src/Core/Challenge/ValidatorInterface.php @@ -25,12 +25,12 @@ interface ValidatorInterface * * @return bool The validator supports the given challenge's type */ - public function supports(AuthorizationChallenge $authorizationChallenge); + public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool; /** * Internally validate the challenge by performing the same kind of test than the CA. * * @return bool The challenge is valid */ - public function isValid(AuthorizationChallenge $authorizationChallenge); + public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool; } diff --git a/src/Core/Challenge/WaitingValidator.php b/src/Core/Challenge/WaitingValidator.php index 07a95647..7c6474c2 100644 --- a/src/Core/Challenge/WaitingValidator.php +++ b/src/Core/Challenge/WaitingValidator.php @@ -21,42 +21,29 @@ */ class WaitingValidator implements ValidatorInterface { - /** - * @var ValidatorInterface - */ + /** @var ValidatorInterface */ private $validator; - /** - * @var int - */ + /** @var int */ private $timeout; - /** - * @param int $timeout - */ - public function __construct(ValidatorInterface $validator, $timeout = 180) + public function __construct(ValidatorInterface $validator, int $timeout = 180) { $this->validator = $validator; $this->timeout = $timeout; } - /** - * {@inheritdoc} - */ - public function supports(AuthorizationChallenge $authorizationChallenge) + public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { - return $this->validator->supports($authorizationChallenge); + return $this->validator->supports($authorizationChallenge, $solver); } - /** - * {@inheritdoc} - */ - public function isValid(AuthorizationChallenge $authorizationChallenge) + public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool { $limitEndTime = time() + $this->timeout; do { - if ($this->validator->isValid($authorizationChallenge)) { + if ($this->validator->isValid($authorizationChallenge, $solver)) { return true; } sleep(3); diff --git a/src/Core/Exception/AcmeCoreClientException.php b/src/Core/Exception/AcmeCoreClientException.php index c452df5f..71a16624 100644 --- a/src/Core/Exception/AcmeCoreClientException.php +++ b/src/Core/Exception/AcmeCoreClientException.php @@ -18,7 +18,7 @@ */ class AcmeCoreClientException extends AcmeCoreException { - public function __construct($message, \Exception $previous = null) + public function __construct($message, ?\Exception $previous = null) { parent::__construct($message, 0, $previous); } diff --git a/src/Core/Exception/AcmeCoreServerException.php b/src/Core/Exception/AcmeCoreServerException.php index 476c3659..f9f88e57 100644 --- a/src/Core/Exception/AcmeCoreServerException.php +++ b/src/Core/Exception/AcmeCoreServerException.php @@ -20,7 +20,7 @@ */ class AcmeCoreServerException extends AcmeCoreException { - public function __construct(RequestInterface $request, $message, \Exception $previous = null) + public function __construct(RequestInterface $request, $message, ?\Exception $previous = null) { parent::__construct($message, $previous ? $previous->getCode() : 0, $previous); } diff --git a/src/Core/Exception/AcmeDnsResolutionException.php b/src/Core/Exception/AcmeDnsResolutionException.php index 8c00aea1..c8340d3f 100644 --- a/src/Core/Exception/AcmeDnsResolutionException.php +++ b/src/Core/Exception/AcmeDnsResolutionException.php @@ -16,7 +16,7 @@ */ class AcmeDnsResolutionException extends AcmeCoreException { - public function __construct($message, \Exception $previous = null) + public function __construct($message, ?\Exception $previous = null) { parent::__construct(null === $message ? 'An exception was thrown during resolution of DNS' : $message, 0, $previous); } diff --git a/src/Core/Exception/Protocol/CertificateRequestFailedException.php b/src/Core/Exception/Protocol/CertificateRequestFailedException.php index e6444f1f..36e12ca8 100644 --- a/src/Core/Exception/Protocol/CertificateRequestFailedException.php +++ b/src/Core/Exception/Protocol/CertificateRequestFailedException.php @@ -16,7 +16,7 @@ */ class CertificateRequestFailedException extends ProtocolException { - public function __construct($response) + public function __construct(string $response) { parent::__construct(sprintf('Certificate request failed (response: %s)', $response)); } diff --git a/src/Core/Exception/Protocol/CertificateRequestTimedOutException.php b/src/Core/Exception/Protocol/CertificateRequestTimedOutException.php index 7c320b69..ca44edef 100644 --- a/src/Core/Exception/Protocol/CertificateRequestTimedOutException.php +++ b/src/Core/Exception/Protocol/CertificateRequestTimedOutException.php @@ -16,7 +16,7 @@ */ class CertificateRequestTimedOutException extends ProtocolException { - public function __construct($response) + public function __construct(string $response) { parent::__construct(sprintf('Certificate request timed out (response: %s)', $response)); } diff --git a/src/Core/Exception/Protocol/ChallengeFailedException.php b/src/Core/Exception/Protocol/ChallengeFailedException.php index fb42cdba..dac02348 100644 --- a/src/Core/Exception/Protocol/ChallengeFailedException.php +++ b/src/Core/Exception/Protocol/ChallengeFailedException.php @@ -18,7 +18,7 @@ class ChallengeFailedException extends ProtocolException { private $response; - public function __construct($response, \Exception $previous = null) + public function __construct($response, ?\Exception $previous = null) { parent::__construct( sprintf('Challenge failed (response: %s).', json_encode($response)), diff --git a/src/Core/Exception/Protocol/ChallengeNotSupportedException.php b/src/Core/Exception/Protocol/ChallengeNotSupportedException.php index f60a9bb9..01165824 100644 --- a/src/Core/Exception/Protocol/ChallengeNotSupportedException.php +++ b/src/Core/Exception/Protocol/ChallengeNotSupportedException.php @@ -16,7 +16,7 @@ */ class ChallengeNotSupportedException extends ProtocolException { - public function __construct(\Exception $previous = null) + public function __construct(?\Exception $previous = null) { parent::__construct('This ACME server does not expose supported challenge.', $previous); } diff --git a/src/Core/Exception/Protocol/ChallengeTimedOutException.php b/src/Core/Exception/Protocol/ChallengeTimedOutException.php index ffed7123..aaf0cf0b 100644 --- a/src/Core/Exception/Protocol/ChallengeTimedOutException.php +++ b/src/Core/Exception/Protocol/ChallengeTimedOutException.php @@ -18,7 +18,7 @@ class ChallengeTimedOutException extends ProtocolException { private $response; - public function __construct($response, \Exception $previous = null) + public function __construct($response, ?\Exception $previous = null) { parent::__construct( sprintf('Challenge timed out (response: %s).', json_encode($response)), diff --git a/src/Core/Exception/Server/BadCsrServerException.php b/src/Core/Exception/Server/BadCsrServerException.php index 2a211b8e..b3a6a035 100644 --- a/src/Core/Exception/Server/BadCsrServerException.php +++ b/src/Core/Exception/Server/BadCsrServerException.php @@ -19,7 +19,7 @@ */ class BadCsrServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/BadNonceServerException.php b/src/Core/Exception/Server/BadNonceServerException.php index 61746c08..272d276b 100644 --- a/src/Core/Exception/Server/BadNonceServerException.php +++ b/src/Core/Exception/Server/BadNonceServerException.php @@ -19,7 +19,7 @@ */ class BadNonceServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/CaaServerException.php b/src/Core/Exception/Server/CaaServerException.php new file mode 100644 index 00000000..abde3db7 --- /dev/null +++ b/src/Core/Exception/Server/CaaServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class CaaServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[caa] Certification Authority Authorization (CAA) records forbid the CA from issuing a certificate: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/ConnectionServerException.php b/src/Core/Exception/Server/ConnectionServerException.php index 6fa70ef4..841368f7 100644 --- a/src/Core/Exception/Server/ConnectionServerException.php +++ b/src/Core/Exception/Server/ConnectionServerException.php @@ -19,7 +19,7 @@ */ class ConnectionServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/DnsServerException.php b/src/Core/Exception/Server/DnsServerException.php new file mode 100644 index 00000000..05b1cb78 --- /dev/null +++ b/src/Core/Exception/Server/DnsServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class DnsServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[dns] There was a problem with a DNS query during identifier validation: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/IncorrectResponseServerException.php b/src/Core/Exception/Server/IncorrectResponseServerException.php new file mode 100644 index 00000000..f528e625 --- /dev/null +++ b/src/Core/Exception/Server/IncorrectResponseServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class IncorrectResponseServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + "[incorrectResponse] Response received didn’t match the challenge's requirements: ".$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/InternalServerException.php b/src/Core/Exception/Server/InternalServerException.php index cf0e4923..bfaa9097 100644 --- a/src/Core/Exception/Server/InternalServerException.php +++ b/src/Core/Exception/Server/InternalServerException.php @@ -19,7 +19,7 @@ */ class InternalServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/InvalidContactServerException.php b/src/Core/Exception/Server/InvalidContactServerException.php new file mode 100644 index 00000000..eadb7d3b --- /dev/null +++ b/src/Core/Exception/Server/InvalidContactServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class InvalidContactServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[invalidContact] A contact URL for an account was invalid: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/InvalidEmailServerException.php b/src/Core/Exception/Server/InvalidEmailServerException.php index b6a13525..bef0fa74 100644 --- a/src/Core/Exception/Server/InvalidEmailServerException.php +++ b/src/Core/Exception/Server/InvalidEmailServerException.php @@ -19,7 +19,7 @@ */ class InvalidEmailServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/MalformedServerException.php b/src/Core/Exception/Server/MalformedServerException.php index 198c6d87..5358bf54 100644 --- a/src/Core/Exception/Server/MalformedServerException.php +++ b/src/Core/Exception/Server/MalformedServerException.php @@ -19,7 +19,7 @@ */ class MalformedServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/OrderNotReadyServerException.php b/src/Core/Exception/Server/OrderNotReadyServerException.php index 462cbf4e..9c584284 100644 --- a/src/Core/Exception/Server/OrderNotReadyServerException.php +++ b/src/Core/Exception/Server/OrderNotReadyServerException.php @@ -16,7 +16,7 @@ class OrderNotReadyServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/RateLimitedServerException.php b/src/Core/Exception/Server/RateLimitedServerException.php index 5f2e7f62..f0190b4f 100644 --- a/src/Core/Exception/Server/RateLimitedServerException.php +++ b/src/Core/Exception/Server/RateLimitedServerException.php @@ -19,7 +19,7 @@ */ class RateLimitedServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/RejectedIdentifierServerException.php b/src/Core/Exception/Server/RejectedIdentifierServerException.php new file mode 100644 index 00000000..fa4e1dde --- /dev/null +++ b/src/Core/Exception/Server/RejectedIdentifierServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class RejectedIdentifierServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[rejectedIdentifier] The server will not issue certificates for the identifier: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/TlsServerException.php b/src/Core/Exception/Server/TlsServerException.php index 903fbf51..45c50d21 100644 --- a/src/Core/Exception/Server/TlsServerException.php +++ b/src/Core/Exception/Server/TlsServerException.php @@ -19,7 +19,7 @@ */ class TlsServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/UnauthorizedServerException.php b/src/Core/Exception/Server/UnauthorizedServerException.php index 8457f0e4..c76343b3 100644 --- a/src/Core/Exception/Server/UnauthorizedServerException.php +++ b/src/Core/Exception/Server/UnauthorizedServerException.php @@ -19,7 +19,7 @@ */ class UnauthorizedServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/UnknownHostServerException.php b/src/Core/Exception/Server/UnknownHostServerException.php index 6fdab76d..671f366c 100644 --- a/src/Core/Exception/Server/UnknownHostServerException.php +++ b/src/Core/Exception/Server/UnknownHostServerException.php @@ -19,7 +19,7 @@ */ class UnknownHostServerException extends AcmeCoreServerException { - public function __construct(RequestInterface $request, $detail, \Exception $previous = null) + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) { parent::__construct( $request, diff --git a/src/Core/Exception/Server/UnsupportedContactServerException.php b/src/Core/Exception/Server/UnsupportedContactServerException.php new file mode 100644 index 00000000..e34b4d5e --- /dev/null +++ b/src/Core/Exception/Server/UnsupportedContactServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class UnsupportedContactServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[unsupportedContact] A contact URL for an account used an unsupported protocol scheme: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/UnsupportedIdentifierServerException.php b/src/Core/Exception/Server/UnsupportedIdentifierServerException.php new file mode 100644 index 00000000..e97db3c4 --- /dev/null +++ b/src/Core/Exception/Server/UnsupportedIdentifierServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class UnsupportedIdentifierServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[unsupportedIdentifier] An identifier is of an unsupported type: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Exception/Server/UserActionRequiredServerException.php b/src/Core/Exception/Server/UserActionRequiredServerException.php new file mode 100644 index 00000000..a457d844 --- /dev/null +++ b/src/Core/Exception/Server/UserActionRequiredServerException.php @@ -0,0 +1,30 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Exception\Server; + +use AcmePhp\Core\Exception\AcmeCoreServerException; +use Psr\Http\Message\RequestInterface; + +/** + * @author Alex Plekhanov + */ +class UserActionRequiredServerException extends AcmeCoreServerException +{ + public function __construct(RequestInterface $request, string $detail, ?\Exception $previous = null) + { + parent::__construct( + $request, + '[userActionRequired] Visit the “instance” URL and take actions specified there: '.$detail, + $previous + ); + } +} diff --git a/src/Core/Filesystem/Adapter/FlysystemAdapter.php b/src/Core/Filesystem/Adapter/FlysystemAdapter.php index a61cf0b0..f676d4f5 100644 --- a/src/Core/Filesystem/Adapter/FlysystemAdapter.php +++ b/src/Core/Filesystem/Adapter/FlysystemAdapter.php @@ -12,7 +12,8 @@ namespace AcmePhp\Core\Filesystem\Adapter; use AcmePhp\Core\Filesystem\FilesystemInterface; -use League\Flysystem\FilesystemInterface as FlysystemFilesystemInterface; +use League\Flysystem\FilesystemException; +use League\Flysystem\FilesystemOperator as FlysystemFilesystemInterface; class FlysystemAdapter implements FilesystemInterface { @@ -26,47 +27,44 @@ public function __construct(FlysystemFilesystemInterface $filesystem) $this->filesystem = $filesystem; } - public function write($path, $content) + public function write(string $path, string $content) { - $isOnRemote = $this->filesystem->has($path); - if ($isOnRemote && !$this->filesystem->update($path, $content)) { - throw $this->createRuntimeException($path, 'updated'); - } - if (!$isOnRemote && !$this->filesystem->write($path, $content)) { - throw $this->createRuntimeException($path, 'created'); + try { + $this->filesystem->write($path, $content); + } catch (FilesystemException $e) { + throw $this->createRuntimeException($path, 'created', $e); } } - public function delete($path) + public function delete(string $path) { $isOnRemote = $this->filesystem->has($path); - if ($isOnRemote && !$this->filesystem->delete($path)) { - throw $this->createRuntimeException($path, 'delete'); + try { + if ($isOnRemote) { + $this->filesystem->delete($path); + } + } catch (FilesystemException $e) { + throw $this->createRuntimeException($path, 'deleted', $e); } } - public function createDir($path) + public function createDir(string $path) { - $isOnRemote = $this->filesystem->has($path); - if (!$isOnRemote && !$this->filesystem->createDir($path)) { - throw $this->createRuntimeException($path, 'created'); + try { + $this->filesystem->createDirectory($path); + } catch (FilesystemException $e) { + throw $this->createRuntimeException($path, 'created', $e); } } - /** - * @param string $path - * @param string $action - * - * @return \RuntimeException - */ - private function createRuntimeException($path, $action) + private function createRuntimeException(string $path, string $action, FilesystemException $e): \RuntimeException { return new \RuntimeException( sprintf( 'File %s could not be %s because: %s', $path, $action, - error_get_last() + $e->getMessage(), ) ); } diff --git a/src/Core/Filesystem/Adapter/FlysystemFtpFactory.php b/src/Core/Filesystem/Adapter/FlysystemFtpFactory.php index 58f4b9ee..e43b7586 100644 --- a/src/Core/Filesystem/Adapter/FlysystemFtpFactory.php +++ b/src/Core/Filesystem/Adapter/FlysystemFtpFactory.php @@ -12,15 +12,13 @@ namespace AcmePhp\Core\Filesystem\Adapter; use AcmePhp\Core\Filesystem\FilesystemFactoryInterface; +use AcmePhp\Core\Filesystem\FilesystemInterface; use League\Flysystem\Adapter\Ftp; use League\Flysystem\Filesystem; class FlysystemFtpFactory implements FilesystemFactoryInterface { - /** - * {@inheritdoc} - */ - public function create(array $config) + public function create(array $config): FilesystemInterface { return new FlysystemAdapter(new Filesystem(new Ftp($config))); } diff --git a/src/Core/Filesystem/Adapter/FlysystemLocalFactory.php b/src/Core/Filesystem/Adapter/FlysystemLocalFactory.php index b19d9f45..6e21ff17 100644 --- a/src/Core/Filesystem/Adapter/FlysystemLocalFactory.php +++ b/src/Core/Filesystem/Adapter/FlysystemLocalFactory.php @@ -12,16 +12,14 @@ namespace AcmePhp\Core\Filesystem\Adapter; use AcmePhp\Core\Filesystem\FilesystemFactoryInterface; +use AcmePhp\Core\Filesystem\FilesystemInterface; use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; use Webmozart\Assert\Assert; class FlysystemLocalFactory implements FilesystemFactoryInterface { - /** - * {@inheritdoc} - */ - public function create(array $config) + public function create(array $config): FilesystemInterface { Assert::keyExists($config, 'root', 'create::$config expected an array with the key %s.'); diff --git a/src/Core/Filesystem/Adapter/FlysystemSftpFactory.php b/src/Core/Filesystem/Adapter/FlysystemSftpFactory.php index b2c1b2d3..ffd38828 100644 --- a/src/Core/Filesystem/Adapter/FlysystemSftpFactory.php +++ b/src/Core/Filesystem/Adapter/FlysystemSftpFactory.php @@ -12,16 +12,27 @@ namespace AcmePhp\Core\Filesystem\Adapter; use AcmePhp\Core\Filesystem\FilesystemFactoryInterface; +use AcmePhp\Core\Filesystem\FilesystemInterface; use League\Flysystem\Filesystem; -use League\Flysystem\Sftp\SftpAdapter; +use League\Flysystem\PhpseclibV3\SftpAdapter; +use League\Flysystem\PhpseclibV3\SftpConnectionProvider; class FlysystemSftpFactory implements FilesystemFactoryInterface { - /** - * {@inheritdoc} - */ - public function create(array $config) + public function create(array $config): FilesystemInterface { - return new FlysystemAdapter(new Filesystem(new SftpAdapter($config))); + return new FlysystemAdapter( + new Filesystem( + new SftpAdapter( + new SftpConnectionProvider( + $config['host'], + $config['username'], + password: $config['password'] ?? null, + port: $config['port'] ?? 22, + ), + $config['root'] ?? '/', + ) + ) + ); } } diff --git a/src/Core/Filesystem/Adapter/NullAdapter.php b/src/Core/Filesystem/Adapter/NullAdapter.php index 169221aa..3f22d45f 100644 --- a/src/Core/Filesystem/Adapter/NullAdapter.php +++ b/src/Core/Filesystem/Adapter/NullAdapter.php @@ -12,12 +12,12 @@ namespace AcmePhp\Core\Filesystem\Adapter; use League\Flysystem\Filesystem; -use League\Flysystem\Memory\MemoryAdapter; +use League\Flysystem\InMemory\InMemoryFilesystemAdapter; class NullAdapter extends FlysystemAdapter { public function __construct() { - parent::__construct(new Filesystem(new MemoryAdapter())); + parent::__construct(new Filesystem(new InMemoryFilesystemAdapter())); } } diff --git a/src/Core/Filesystem/FilesystemFactoryInterface.php b/src/Core/Filesystem/FilesystemFactoryInterface.php index 11377b6f..48b04d25 100644 --- a/src/Core/Filesystem/FilesystemFactoryInterface.php +++ b/src/Core/Filesystem/FilesystemFactoryInterface.php @@ -15,8 +15,6 @@ interface FilesystemFactoryInterface { /** * Create a new Filesystem. - * - * @return FilesystemInterface */ - public function create(array $config); + public function create(array $config): FilesystemInterface; } diff --git a/src/Core/Filesystem/FilesystemInterface.php b/src/Core/Filesystem/FilesystemInterface.php index c8843cd5..f7f6fae0 100644 --- a/src/Core/Filesystem/FilesystemInterface.php +++ b/src/Core/Filesystem/FilesystemInterface.php @@ -15,23 +15,16 @@ interface FilesystemInterface { /** * Write content to a file. - * - * @param string $path - * @param string $content */ - public function write($path, $content); + public function write(string $path, string $content); /** * Delete a file. - * - * @param string $path */ - public function delete($path); + public function delete(string $path); /** * Delete a directory. - * - * @param string $path */ - public function createDir($path); + public function createDir(string $path); } diff --git a/src/Core/Http/Base64SafeEncoder.php b/src/Core/Http/Base64SafeEncoder.php index 6370ff6f..f6d6796b 100644 --- a/src/Core/Http/Base64SafeEncoder.php +++ b/src/Core/Http/Base64SafeEncoder.php @@ -18,22 +18,12 @@ */ class Base64SafeEncoder { - /** - * @param string $input - * - * @return string - */ - public function encode($input) + public function encode(string $input): string { return str_replace('=', '', strtr(base64_encode($input), '+/', '-_')); } - /** - * @param string $input - * - * @return string - */ - public function decode($input) + public function decode(string $input): string { $remainder = \strlen($input) % 4; diff --git a/src/Core/Http/SecureHttpClient.php b/src/Core/Http/SecureHttpClient.php index 862153d6..4172f1bf 100644 --- a/src/Core/Http/SecureHttpClient.php +++ b/src/Core/Http/SecureHttpClient.php @@ -15,13 +15,18 @@ use AcmePhp\Core\Exception\AcmeCoreServerException; use AcmePhp\Core\Exception\Protocol\ExpectedJsonException; use AcmePhp\Core\Exception\Server\BadNonceServerException; +use AcmePhp\Core\Protocol\ExternalAccount; use AcmePhp\Core\Util\JsonDecoder; use AcmePhp\Ssl\KeyPair; use AcmePhp\Ssl\Parser\KeyParser; use AcmePhp\Ssl\Signer\DataSigner; use GuzzleHttp\ClientInterface; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Header; use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Utils; +use Lcobucci\JWT\Signer\Hmac\Sha256; +use Lcobucci\JWT\Signer\Key\InMemory; use Psr\Http\Message\ResponseInterface; /** @@ -87,73 +92,7 @@ public function __construct( $this->errorHandler = $errorHandler; } - /** - * Send a request encoded in the format defined by the ACME protocol. - * - * @param string $method - * @param string $endpoint - * @param bool $returnJson - * - * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code - * @throws AcmeCoreClientException when an error occured during response parsing - * - * @return array|string Array of parsed JSON if $returnJson = true, string otherwise - */ - public function signedRequest($method, $endpoint, array $payload = [], $returnJson = true) - { - @trigger_error('The method signedRequest is deprecated since version 1.1 and will be removed in 2.0. use methods request, signKidPayload instead', E_USER_DEPRECATED); - - return $this->request($method, $endpoint, $this->signJwkPayload($endpoint, $payload), $returnJson); - } - - private function getAlg() - { - $privateKey = $this->accountKeyPair->getPrivateKey(); - $parsedKey = $this->keyParser->parse($privateKey); - switch ($parsedKey->getType()) { - case OPENSSL_KEYTYPE_RSA: - return 'RS256'; - case OPENSSL_KEYTYPE_EC: - switch ($parsedKey->getBits()) { - case 256: - case 384: - return 'ES'.$parsedKey->getBits(); - case 521: - return 'ES512'; - } - // no break to let the default case - default: - throw new AcmeCoreClientException('Private key type is not supported'); - } - } - - private function extractSignOptionFromJWSAlg($alg) - { - if (!preg_match('/^([A-Z]+)(\d+)$/', $alg, $match)) { - throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); - } - - if (!\defined('OPENSSL_ALGO_SHA'.$match[2])) { - throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); - } - - $algorithm = \constant('OPENSSL_ALGO_SHA'.$match[2]); - - switch ($match[1]) { - case 'RS': - $format = DataSigner::FORMAT_DER; - break; - case 'ES': - $format = DataSigner::FORMAT_ECDSA; - break; - default: - throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); - } - - return [$algorithm, $format]; - } - - public function getJWK() + public function getJWK(): array { $privateKey = $this->accountKeyPair->getPrivateKey(); $parsedKey = $this->keyParser->parse($privateKey); @@ -166,6 +105,7 @@ public function getJWK() 'kty' => 'RSA', 'n' => $this->base64Encoder->encode($parsedKey->getDetail('n')), ]; + case OPENSSL_KEYTYPE_EC: return [ // this order matters @@ -174,12 +114,13 @@ public function getJWK() 'x' => $this->base64Encoder->encode($parsedKey->getDetail('x')), 'y' => $this->base64Encoder->encode($parsedKey->getDetail('y')), ]; + default: throw new AcmeCoreClientException('Private key type not supported'); } } - public function getJWKThumbprint() + public function getJWKThumbprint(): string { return hash('sha256', json_encode($this->getJWK()), true); } @@ -187,138 +128,104 @@ public function getJWKThumbprint() /** * Generates a payload signed with account's KID. * - * @param string $endpoint - * @param string $account - * @param array $payload - * - * @return array the signed Pyaload + * @param string|array|null $payload */ - public function signKidPayload($endpoint, $account, array $payload = null) + public function signKidPayload(string $endpoint, string $account, $payload = null, bool $withNonce = true): array { - return $this->signPayload( - [ - 'alg' => $this->getAlg(), - 'kid' => $account, - 'nonce' => $this->getNonce(), - 'url' => $endpoint, - ], - $payload - ); + $protected = ['alg' => $this->getAlg(), 'kid' => $account, 'url' => $endpoint]; + if ($withNonce) { + $protected['nonce'] = $this->getNonce(); + } + + return $this->signPayload($protected, $payload); } /** * Generates a payload signed with JWK. * - * @param string $endpoint - * @param array $payload - * - * @return array the signed Payload + * @param string|array|null $payload */ - public function signJwkPayload($endpoint, array $payload = null) + public function signJwkPayload(string $endpoint, $payload = null, bool $withNonce = true): array { - return $this->signPayload( - [ - 'alg' => $this->getAlg(), - 'jwk' => $this->getJWK(), - 'nonce' => $this->getNonce(), - 'url' => $endpoint, - ], - $payload - ); + $protected = ['alg' => $this->getAlg(), 'jwk' => $this->getJWK(), 'url' => $endpoint]; + if ($withNonce) { + $protected['nonce'] = $this->getNonce(); + } + + return $this->signPayload($protected, $payload); } /** - * Sign the given Payload. - * - * @return array + * Generates an External Account Binding payload signed with JWS. */ - private function signPayload(array $protected, array $payload = null) + public function createExternalAccountPayload(ExternalAccount $externalAccount, string $url): array { - if (!isset($protected['alg'])) { - throw new \InvalidArgumentException('The property "alg" is required in the protected array'); - } - $alg = $protected['alg']; + $signer = new Sha256(); - $privateKey = $this->accountKeyPair->getPrivateKey(); - list($algorithm, $format) = $this->extractSignOptionFromJWSAlg($alg); + $protected = [ + 'alg' => $signer->algorithmId(), + 'kid' => $externalAccount->getId(), + 'url' => $url, + ]; - $protected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES)); - if (null === $payload) { - $payload = ''; - } elseif ($payload === []) { - $payload = $this->base64Encoder->encode('{}'); - } else { - $payload = $this->base64Encoder->encode(json_encode($payload, JSON_UNESCAPED_SLASHES)); - } - $signature = $this->base64Encoder->encode( - $this->dataSigner->signData($protected.'.'.$payload, $privateKey, $algorithm, $format) - ); + $encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES)); + $encodedPayload = $this->base64Encoder->encode(json_encode($this->getJWK(), JSON_UNESCAPED_SLASHES)); + + $hmacKey = $this->base64Encoder->decode($externalAccount->getHmacKey()); + $hmacKey = class_exists(InMemory::class) ? InMemory::plainText($hmacKey) : $hmacKey; + + $signature = $this->base64Encoder->encode($signer->sign($encodedProtected.'.'.$encodedPayload, $hmacKey)); return [ - 'protected' => $protected, - 'payload' => $payload, + 'protected' => $encodedProtected, + 'payload' => $encodedPayload, 'signature' => $signature, ]; } /** - * Send a request encoded in the format defined by the ACME protocol. - * - * @param string $method - * @param string $endpoint - * @param string $account - * @param bool $returnJson - * - * @throws AcmeCoreClientException when an error occured during response parsing - * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code + * Send a request encoded in the format defined by the ACME protocol + * and its content (optionally parsed as JSON). * * @return array|string Array of parsed JSON if $returnJson = true, string otherwise - */ - public function signedKidRequest($method, $endpoint, $account, array $payload = [], $returnJson = true) - { - @trigger_error('The method signedKidRequest is deprecated since version 1.1 and will be removed in 2.0. use methods request, signKidPayload instead.', E_USER_DEPRECATED); - - return $this->request($method, $endpoint, $this->signKidPayload($endpoint, $account, $payload), $returnJson); - } - - /** - * Send a request encoded in the format defined by the ACME protocol. - * - * @param string $method - * @param string $endpoint - * @param array $data - * @param bool $returnJson * * @throws AcmeCoreClientException when an error occured during response parsing * @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code - * - * @return array|string Array of parsed JSON if $returnJson = true, string otherwise */ - public function unsignedRequest($method, $endpoint, array $data = null, $returnJson = true) + public function request(string $method, string $endpoint, array $data = [], bool $returnJson = true) { - @trigger_error('The method unsignedRequest is deprecated since version 1.1 and will be removed in 2.0. use methods request instead.', E_USER_DEPRECATED); + $response = $this->rawRequest($method, $endpoint, $data, $returnJson); + $body = Utils::copyToString($response->getBody()); + + if (!$returnJson) { + return $body; + } + + try { + if ('' === $body) { + throw new \InvalidArgumentException('Empty body received.'); + } - return $this->request($method, $endpoint, $data, $returnJson); + $data = JsonDecoder::decode($body, true); + } catch (\InvalidArgumentException $exception) { + throw new ExpectedJsonException(sprintf('ACME client expected valid JSON as a response to request "%s %s" (given: "%s")', $method, $endpoint, ServerErrorHandler::getResponseBodySummary($response)), $exception); + } + + return $data; } /** - * Send a request encoded in the format defined by the ACME protocol. - * - * @param string $method - * @param string $endpoint - * @param bool $returnJson + * Send a request encoded in the format defined by the ACME protocol and return the response object. * * @throws AcmeCoreClientException when an error occured during response parsing * @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON * @throws AcmeCoreServerException when the ACME server returns an error HTTP status code - * - * @return array|string Array of parsed JSON if $returnJson = true, string otherwise */ - public function request($method, $endpoint, array $data = [], $returnJson = true) + public function rawRequest(string $method, string $endpoint, array $data = [], bool $acceptJson = false): ResponseInterface { - $call = function () use ($method, $endpoint, $data) { - $request = $this->createRequest($method, $endpoint, $data); + $call = function () use ($method, $endpoint, $data, $acceptJson) { + $request = $this->createRequest($method, $endpoint, $data, $acceptJson); try { $this->lastResponse = $this->httpClient->send($request); } catch (\Exception $exception) { @@ -329,28 +236,12 @@ public function request($method, $endpoint, array $data = [], $returnJson = true }; try { - $request = $call(); + $call(); } catch (BadNonceServerException $e) { - $request = $call(); - } - - $body = \GuzzleHttp\Psr7\copy_to_string($this->lastResponse->getBody()); - - if (!$returnJson) { - return $body; - } - - try { - if ('' === $body) { - throw new \InvalidArgumentException('Empty body received.'); - } - - $data = JsonDecoder::decode($body, true); - } catch (\InvalidArgumentException $exception) { - throw new ExpectedJsonException(sprintf('ACME client excepted valid JSON as a response to request "%s %s" (given: "%s")', $request->getMethod(), $request->getUri(), ServerErrorHandler::getResponseBodySummary($this->lastResponse)), $exception); + $call(); } - return $data; + return $this->lastResponse; } public function setAccountKeyPair(KeyPair $keyPair) @@ -358,78 +249,94 @@ public function setAccountKeyPair(KeyPair $keyPair) $this->accountKeyPair = $keyPair; } - /** - * @return int - */ - public function getLastCode() + public function getLastCode(): int { return $this->lastResponse->getStatusCode(); } - /** - * @return string - */ - public function getLastLocation() + public function getLastLocation(): string { return $this->lastResponse->getHeaderLine('Location'); } - /** - * @return array - */ - public function getLastLinks() + public function getLastLinks(): array { - return \GuzzleHttp\Psr7\parse_header($this->lastResponse->getHeader('Link')); + return Header::parse($this->lastResponse->getHeader('Link')); } - /** - * @return KeyPair - */ - public function getAccountKeyPair() + public function getAccountKeyPair(): KeyPair { return $this->accountKeyPair; } - /** - * @return KeyParser - */ - public function getKeyParser() + public function getKeyParser(): KeyParser { return $this->keyParser; } - /** - * @return DataSigner - */ - public function getDataSigner() + public function getDataSigner(): DataSigner { return $this->dataSigner; } - /** - * @param string $endpoint - */ - public function setNonceEndpoint($endpoint) + public function setNonceEndpoint(string $endpoint) { $this->nonceEndpoint = $endpoint; } + public function getBase64Encoder(): Base64SafeEncoder + { + return $this->base64Encoder; + } + /** - * @return Base64SafeEncoder + * Sign the given Payload. */ - public function getBase64Encoder() + private function signPayload(array $protected, ?array $payload = null): array { - return $this->base64Encoder; + if (!isset($protected['alg'])) { + throw new \InvalidArgumentException('The property "alg" is required in the protected array'); + } + + $alg = $protected['alg']; + + $privateKey = $this->accountKeyPair->getPrivateKey(); + list($algorithm, $format) = $this->extractSignOptionFromJWSAlg($alg); + + $encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES)); + + if (null === $payload) { + $encodedPayload = ''; + } elseif ([] === $payload) { + $encodedPayload = $this->base64Encoder->encode('{}'); + } else { + $encodedPayload = $this->base64Encoder->encode(json_encode($payload, JSON_UNESCAPED_SLASHES)); + } + + $signature = $this->base64Encoder->encode( + $this->dataSigner->signData($encodedProtected.'.'.$encodedPayload, $privateKey, $algorithm, $format) + ); + + return [ + 'protected' => $encodedProtected, + 'payload' => $encodedPayload, + 'signature' => $signature, + ]; } - private function createRequest($method, $endpoint, $data) + private function createRequest($method, $endpoint, $data, $acceptJson) { $request = new Request($method, $endpoint); - $request = $request->withHeader('Accept', 'application/json,application/jose+json,'); + + if ($acceptJson) { + $request = $request->withHeader('Accept', 'application/json,application/jose+json,'); + } else { + $request = $request->withHeader('Accept', '*/*'); + } if ('POST' === $method && \is_array($data)) { $request = $request->withHeader('Content-Type', 'application/jose+json'); - $request = $request->withBody(\GuzzleHttp\Psr7\stream_for(json_encode($data))); + $request = $request->withBody(Utils::streamFor(json_encode($data))); } return $request; @@ -446,7 +353,7 @@ private function handleClientException(Request $request, \Exception $exception) throw new AcmeCoreClientException(sprintf('An error occured during request "%s %s"', $request->getMethod(), $request->getUri()), $exception); } - private function getNonce() + private function getNonce(): ?string { if ($this->lastResponse && $this->lastResponse->hasHeader('Replay-Nonce')) { return $this->lastResponse->getHeaderLine('Replay-Nonce'); @@ -454,9 +361,64 @@ private function getNonce() if (null !== $this->nonceEndpoint) { $this->request('HEAD', $this->nonceEndpoint, [], false); + if ($this->lastResponse->hasHeader('Replay-Nonce')) { return $this->lastResponse->getHeaderLine('Replay-Nonce'); } } + + return null; + } + + private function getAlg(): string + { + $privateKey = $this->accountKeyPair->getPrivateKey(); + $parsedKey = $this->keyParser->parse($privateKey); + + switch ($parsedKey->getType()) { + case OPENSSL_KEYTYPE_RSA: + return 'RS256'; + + case OPENSSL_KEYTYPE_EC: + switch ($parsedKey->getBits()) { + case 256: + case 384: + return 'ES'.$parsedKey->getBits(); + case 521: + return 'ES512'; + } + + // no break to let the default case + default: + throw new AcmeCoreClientException('Private key type is not supported'); + } + } + + private function extractSignOptionFromJWSAlg($alg): array + { + if (!preg_match('/^([A-Z]+)(\d+)$/', $alg, $match)) { + throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); + } + + if (!\defined('OPENSSL_ALGO_SHA'.$match[2])) { + throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); + } + + $algorithm = \constant('OPENSSL_ALGO_SHA'.$match[2]); + + switch ($match[1]) { + case 'RS': + $format = DataSigner::FORMAT_DER; + break; + + case 'ES': + $format = DataSigner::FORMAT_ECDSA; + break; + + default: + throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg)); + } + + return [$algorithm, $format]; } } diff --git a/src/Core/Http/SecureHttpClientFactory.php b/src/Core/Http/SecureHttpClientFactory.php index e0b0cf0d..f80694ca 100644 --- a/src/Core/Http/SecureHttpClientFactory.php +++ b/src/Core/Http/SecureHttpClientFactory.php @@ -64,10 +64,8 @@ public function __construct( /** * Create a SecureHttpClient using a given account KeyPair. - * - * @return SecureHttpClient */ - public function createSecureHttpClient(KeyPair $accountKeyPair) + public function createSecureHttpClient(KeyPair $accountKeyPair): SecureHttpClient { return new SecureHttpClient( $accountKeyPair, diff --git a/src/Core/Http/ServerErrorHandler.php b/src/Core/Http/ServerErrorHandler.php index dea5e6dc..b9f5301c 100644 --- a/src/Core/Http/ServerErrorHandler.php +++ b/src/Core/Http/ServerErrorHandler.php @@ -14,17 +14,26 @@ use AcmePhp\Core\Exception\AcmeCoreServerException; use AcmePhp\Core\Exception\Server\BadCsrServerException; use AcmePhp\Core\Exception\Server\BadNonceServerException; +use AcmePhp\Core\Exception\Server\CaaServerException; use AcmePhp\Core\Exception\Server\ConnectionServerException; +use AcmePhp\Core\Exception\Server\DnsServerException; +use AcmePhp\Core\Exception\Server\IncorrectResponseServerException; use AcmePhp\Core\Exception\Server\InternalServerException; +use AcmePhp\Core\Exception\Server\InvalidContactServerException; use AcmePhp\Core\Exception\Server\InvalidEmailServerException; use AcmePhp\Core\Exception\Server\MalformedServerException; use AcmePhp\Core\Exception\Server\OrderNotReadyServerException; use AcmePhp\Core\Exception\Server\RateLimitedServerException; +use AcmePhp\Core\Exception\Server\RejectedIdentifierServerException; use AcmePhp\Core\Exception\Server\TlsServerException; use AcmePhp\Core\Exception\Server\UnauthorizedServerException; use AcmePhp\Core\Exception\Server\UnknownHostServerException; +use AcmePhp\Core\Exception\Server\UnsupportedContactServerException; +use AcmePhp\Core\Exception\Server\UnsupportedIdentifierServerException; +use AcmePhp\Core\Exception\Server\UserActionRequiredServerException; use AcmePhp\Core\Util\JsonDecoder; use GuzzleHttp\Exception\RequestException; +use GuzzleHttp\Psr7\Utils; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; @@ -38,24 +47,30 @@ class ServerErrorHandler private static $exceptions = [ 'badCSR' => BadCsrServerException::class, 'badNonce' => BadNonceServerException::class, + 'caa' => CaaServerException::class, 'connection' => ConnectionServerException::class, - 'serverInternal' => InternalServerException::class, + 'dns' => DnsServerException::class, + 'incorrectResponse' => IncorrectResponseServerException::class, + 'invalidContact' => InvalidContactServerException::class, 'invalidEmail' => InvalidEmailServerException::class, 'malformed' => MalformedServerException::class, 'orderNotReady' => OrderNotReadyServerException::class, 'rateLimited' => RateLimitedServerException::class, + 'rejectedIdentifier' => RejectedIdentifierServerException::class, + 'serverInternal' => InternalServerException::class, 'tls' => TlsServerException::class, 'unauthorized' => UnauthorizedServerException::class, 'unknownHost' => UnknownHostServerException::class, + 'unsupportedContact' => UnsupportedContactServerException::class, + 'unsupportedIdentifier' => UnsupportedIdentifierServerException::class, + 'userActionRequired' => UserActionRequiredServerException::class, ]; /** * Get a response summary (useful for exceptions). * Use Guzzle method if available (Guzzle 6.1.1+). - * - * @return string */ - public static function getResponseBodySummary(ResponseInterface $response) + public static function getResponseBodySummary(ResponseInterface $response): string { // Rewind the stream if possible to allow re-reading for the summary. if ($response->getBody()->isSeekable()) { @@ -66,7 +81,7 @@ public static function getResponseBodySummary(ResponseInterface $response) return RequestException::getResponseBodySummary($response); } - $body = \GuzzleHttp\Psr7\copy_to_string($response->getBody()); + $body = Utils::copyToString($response->getBody()); if (\strlen($body) > 120) { return substr($body, 0, 120).' (truncated...)'; @@ -75,15 +90,12 @@ public static function getResponseBodySummary(ResponseInterface $response) return $body; } - /** - * @return AcmeCoreServerException - */ public function createAcmeExceptionForResponse( RequestInterface $request, ResponseInterface $response, - \Exception $previous = null - ) { - $body = \GuzzleHttp\Psr7\copy_to_string($response->getBody()); + ?\Exception $previous = null + ): AcmeCoreServerException { + $body = Utils::copyToString($response->getBody()); try { $data = JsonDecoder::decode($body, true); @@ -112,14 +124,11 @@ public function createAcmeExceptionForResponse( ); } - /** - * @return AcmeCoreServerException - */ private function createDefaultExceptionForResponse( RequestInterface $request, ResponseInterface $response, - \Exception $previous = null - ) { + ?\Exception $previous = null + ): AcmeCoreServerException { return new AcmeCoreServerException( $request, sprintf( diff --git a/src/Core/Protocol/AuthorizationChallenge.php b/src/Core/Protocol/AuthorizationChallenge.php index d0e47586..bfaee7b8 100644 --- a/src/Core/Protocol/AuthorizationChallenge.php +++ b/src/Core/Protocol/AuthorizationChallenge.php @@ -11,8 +11,6 @@ namespace AcmePhp\Core\Protocol; -use Webmozart\Assert\Assert; - /** * Represent a ACME challenge. * @@ -20,53 +18,26 @@ */ class AuthorizationChallenge { - /** - * @var string - */ + /** @var string */ private $domain; - /** - * @var string - */ + /** @var string */ private $status; - /** - * @var string - */ + /** @var string */ private $type; - /** - * @var string - */ + /** @var string */ private $url; - /** - * @var string - */ + /** @var string */ private $token; - /** - * @var string - */ + /** @var string */ private $payload; - /** - * @param string $domain - * @param string $status - * @param string $type - * @param string $url - * @param string $token - * @param string $payload - */ - public function __construct($domain, $status, $type, $url, $token, $payload) + public function __construct(string $domain, string $status, string $type, string $url, string $token, string $payload) { - Assert::stringNotEmpty($domain, 'Challenge::$domain expected a non-empty string. Got: %s'); - Assert::stringNotEmpty($status, 'Challenge::$status expected a non-empty string. Got: %s'); - Assert::stringNotEmpty($type, 'Challenge::$type expected a non-empty string. Got: %s'); - Assert::stringNotEmpty($url, 'Challenge::$url expected a non-empty string. Got: %s'); - Assert::stringNotEmpty($token, 'Challenge::$token expected a non-empty string. Got: %s'); - Assert::stringNotEmpty($payload, 'Challenge::$payload expected a non-empty string. Got: %s'); - $this->domain = $domain; $this->status = $status; $this->type = $type; @@ -75,10 +46,7 @@ public function __construct($domain, $status, $type, $url, $token, $payload) $this->payload = $payload; } - /** - * @return array - */ - public function toArray() + public function toArray(): array { return [ 'domain' => $this->getDomain(), @@ -90,10 +58,7 @@ public function toArray() ]; } - /** - * @return AuthorizationChallenge - */ - public static function fromArray(array $data) + public static function fromArray(array $data): self { return new self( $data['domain'], @@ -105,66 +70,42 @@ public static function fromArray(array $data) ); } - /** - * @return string - */ - public function getDomain() + public function getDomain(): string { return $this->domain; } - /** - * @return string - */ - public function getStatus() + public function getStatus(): string { return $this->status; } - /** - * @return bool - */ - public function isValid() + public function isValid(): bool { return 'valid' === $this->status; } - /** - * @return bool - */ - public function isPending() + public function isPending(): bool { - return 'pending' === $this->status; + return 'pending' === $this->status || 'processing' === $this->status; } - /** - * @return string - */ - public function getType() + public function getType(): string { return $this->type; } - /** - * @return string - */ - public function getUrl() + public function getUrl(): string { return $this->url; } - /** - * @return string - */ - public function getToken() + public function getToken(): string { return $this->token; } - /** - * @return string - */ - public function getPayload() + public function getPayload(): string { return $this->payload; } diff --git a/src/Core/Protocol/CertificateOrder.php b/src/Core/Protocol/CertificateOrder.php index 342bef17..48698fea 100644 --- a/src/Core/Protocol/CertificateOrder.php +++ b/src/Core/Protocol/CertificateOrder.php @@ -12,7 +12,6 @@ namespace AcmePhp\Core\Protocol; use AcmePhp\Core\Exception\AcmeCoreClientException; -use Webmozart\Assert\Assert; /** * Represent an ACME order. @@ -21,29 +20,17 @@ */ class CertificateOrder { - /** - * @var AuthorizationChallenge[][] - */ + /** @var AuthorizationChallenge[][] */ private $authorizationsChallenges; - /** - * @var string - */ + /** @var string */ private $orderEndpoint; - /** - * @param string $domain - * @param string $type - * @param string $url - * @param string $token - * @param string $payload - * @param string $order - */ - public function __construct($authorizationsChallenges, $orderEndpoint = null) - { - Assert::isArray($authorizationsChallenges, 'Challenge::$authorizationsChallenges expected an array. Got: %s'); - Assert::nullOrString($orderEndpoint, 'Challenge::$orderEndpoint expected a string or null. Got: %s'); + /** @var string */ + private $status; + public function __construct(array $authorizationsChallenges, ?string $orderEndpoint = null, ?string $status = null) + { foreach ($authorizationsChallenges as &$authorizationChallenges) { foreach ($authorizationChallenges as &$authorizationChallenge) { if (\is_array($authorizationChallenge)) { @@ -54,28 +41,33 @@ public function __construct($authorizationsChallenges, $orderEndpoint = null) $this->authorizationsChallenges = $authorizationsChallenges; $this->orderEndpoint = $orderEndpoint; + $this->status = $status; } - /** - * @return array - */ - public function toArray() + public function toArray(): array { + $authorizationsChallenges = array_map( + function ($challenges): array { + return array_map( + function ($challenge): array { + return $challenge->toArray(); + }, + $challenges + ); + }, + $this->getAuthorizationsChallenges() + ); + return [ - 'authorizationsChallenges' => $this->getAuthorizationsChallenges(), + 'authorizationsChallenges' => $authorizationsChallenges, 'orderEndpoint' => $this->getOrderEndpoint(), + 'status' => $this->getStatus(), ]; } - /** - * @return AuthorizationChallenge - */ - public static function fromArray(array $data) + public static function fromArray(array $data): self { - return new self( - $data['authorizationsChallenges'], - $data['orderEndpoint'] - ); + return new self($data['authorizationsChallenges'], $data['orderEndpoint']); } /** @@ -87,12 +79,12 @@ public function getAuthorizationsChallenges() } /** - * @param string $domain - * * @return AuthorizationChallenge[] */ - public function getAuthorizationChallenges($domain) + public function getAuthorizationChallenges(string $domain): array { + $domain = strtolower($domain); + if (!isset($this->authorizationsChallenges[$domain])) { throw new AcmeCoreClientException('The order does not contains any authorization challenge for the domain '.$domain); } @@ -100,11 +92,13 @@ public function getAuthorizationChallenges($domain) return $this->authorizationsChallenges[$domain]; } - /** - * @return string - */ - public function getOrderEndpoint() + public function getOrderEndpoint(): string { return $this->orderEndpoint; } + + public function getStatus(): string + { + return $this->status; + } } diff --git a/src/Core/Protocol/ExternalAccount.php b/src/Core/Protocol/ExternalAccount.php new file mode 100644 index 00000000..eb70f96e --- /dev/null +++ b/src/Core/Protocol/ExternalAccount.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace AcmePhp\Core\Protocol; + +/** + * Represent an ACME External Account to be used for External Account Binding. + * + * @author Titouan Galopin + */ +class ExternalAccount +{ + /** @var string */ + private $id; + + /** @var string */ + private $hmacKey; + + public function __construct(string $id, string $hmacKey) + { + $this->id = $id; + $this->hmacKey = $hmacKey; + } + + public function getId(): string + { + return $this->id; + } + + public function getHmacKey(): string + { + return $this->hmacKey; + } +} diff --git a/src/Core/Protocol/ResourcesDirectory.php b/src/Core/Protocol/ResourcesDirectory.php index 589ed6b5..b3c34d6c 100644 --- a/src/Core/Protocol/ResourcesDirectory.php +++ b/src/Core/Protocol/ResourcesDirectory.php @@ -20,14 +20,12 @@ */ class ResourcesDirectory { - const NEW_ACCOUNT = 'newAccount'; - const NEW_ORDER = 'newOrder'; - const NEW_NONCE = 'newNonce'; - const REVOKE_CERT = 'revokeCert'; + public const NEW_ACCOUNT = 'newAccount'; + public const NEW_ORDER = 'newOrder'; + public const NEW_NONCE = 'newNonce'; + public const REVOKE_CERT = 'revokeCert'; - /** - * @var array - */ + /** @var array */ private $serverResources; public function __construct(array $serverResources) @@ -38,7 +36,7 @@ public function __construct(array $serverResources) /** * @return string[] */ - public static function getResourcesNames() + public static function getResourcesNames(): array { return [ self::NEW_ACCOUNT, @@ -50,19 +48,15 @@ public static function getResourcesNames() /** * Find a resource URL. - * - * @param string $resource - * - * @return string */ - public function getResourceUrl($resource) + public function getResourceUrl(string $resource): string { Assert::oneOf( $resource, - self::getResourcesNames(), + array_keys($this->serverResources), 'Resource type "%s" is not supported by the ACME server (supported: %2$s)' ); - return isset($this->serverResources[$resource]) ? $this->serverResources[$resource] : null; + return $this->serverResources[$resource]; } } diff --git a/src/Core/Protocol/RevocationReason.php b/src/Core/Protocol/RevocationReason.php index e7995ed9..19ca9072 100644 --- a/src/Core/Protocol/RevocationReason.php +++ b/src/Core/Protocol/RevocationReason.php @@ -18,55 +18,36 @@ */ class RevocationReason { - const DEFAULT_REASON = self::REASON_UNSPECIFIED; - const REASON_UNSPECIFIED = 0; - const REASON_KEY_COMPROMISE = 1; - const REASON_AFFILLIATION_CHANGED = 3; - const REASON_SUPERCEDED = 4; - const REASON_CESSATION_OF_OPERATION = 5; + public const DEFAULT_REASON = self::REASON_UNSPECIFIED; + public const REASON_UNSPECIFIED = 0; + public const REASON_KEY_COMPROMISE = 1; + public const REASON_AFFILLIATION_CHANGED = 3; + public const REASON_SUPERCEDED = 4; + public const REASON_CESSATION_OF_OPERATION = 5; - /** - * @var int|null - */ - private $reasonType = null; + /** @var int|null */ + private $reasonType; - /** - * @param int $reasonType - * - * @throws \InvalidArgumentException - */ - public function __construct($reasonType) + public function __construct(int $reasonType) { - $reasonType = (int) $reasonType; - Assert::oneOf($reasonType, self::getReasons(), 'Revocation reason type "%s" is not supported by the ACME server (supported: %2$s)'); $this->reasonType = $reasonType; } - /** - * @return int - */ - public function getReasonType() + public function getReasonType(): int { return $this->reasonType; } - /** - * @return static - */ - public static function createDefaultReason() + public static function createDefaultReason(): self { return new static(self::DEFAULT_REASON); } - /** - * @return array - */ - public static function getFormattedReasons() + public static function getFormattedReasons(): array { $formatted = []; - foreach (self::getReasonLabelMap() as $reason => $label) { $formatted[] = $reason.' - '.$label; } @@ -74,10 +55,7 @@ public static function getFormattedReasons() return $formatted; } - /** - * @return array - */ - private static function getReasonLabelMap() + private static function getReasonLabelMap(): array { return [ self::REASON_UNSPECIFIED => 'unspecified', @@ -88,10 +66,7 @@ private static function getReasonLabelMap() ]; } - /** - * @return array - */ - public static function getReasons() + public static function getReasons(): array { return [ self::REASON_UNSPECIFIED, diff --git a/src/Core/Util/JsonDecoder.php b/src/Core/Util/JsonDecoder.php index 77c8bd9a..a783641c 100644 --- a/src/Core/Util/JsonDecoder.php +++ b/src/Core/Util/JsonDecoder.php @@ -32,11 +32,9 @@ class JsonDecoder * * @throws \InvalidArgumentException if the JSON cannot be decoded * - * @return mixed - * * @see http://www.php.net/manual/en/function.json-decode.php */ - public static function decode($json, $assoc = false, $depth = 512, $options = 0) + public static function decode(string $json, bool $assoc = false, int $depth = 512, int $options = 0) { $data = json_decode($json, $assoc, $depth, $options); diff --git a/src/Core/composer.json b/src/Core/composer.json index e512ecc6..80f67104 100644 --- a/src/Core/composer.json +++ b/src/Core/composer.json @@ -27,29 +27,24 @@ } ], "require": { - "php": ">=5.5.0", + "php": ">=8.3", "ext-hash": "*", "ext-json": "*", "ext-openssl": "*", - "acmephp/ssl": "^1.0", - "guzzlehttp/guzzle": "^6.0", - "guzzlehttp/psr7": "^1.0", - "psr/http-message": "^1.0", - "psr/log": "^1.0", + "acmephp/ssl": "^2.0", + "guzzlehttp/guzzle": "^7.2", + "guzzlehttp/psr7": "^2.4.5", + "lcobucci/jwt": "^5.3", + "psr/http-message": "^1 || ^2", + "psr/log": "^2 || ^3", "webmozart/assert": "^1.0" }, - "require-dev": { - "phpunit/phpunit": "^4.8.22", - "symfony/process": "^3.0" - }, "autoload": { "psr-4": { "AcmePhp\\Core\\": "." } }, - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "config": { + "sort-packages": true } } diff --git a/src/Ssl/Certificate.php b/src/Ssl/Certificate.php index c7bbab55..3d12ab5c 100644 --- a/src/Ssl/Certificate.php +++ b/src/Ssl/Certificate.php @@ -27,11 +27,7 @@ class Certificate /** @var Certificate */ private $issuerCertificate; - /** - * @param string $certificatePEM - * @param Certificate|null $issuerCertificate - */ - public function __construct($certificatePEM, self $issuerCertificate = null) + public function __construct(string $certificatePEM, ?self $issuerCertificate = null) { Assert::stringNotEmpty($certificatePEM, __CLASS__.'::$certificatePEM should not be an empty string. Got %s'); @@ -42,7 +38,7 @@ public function __construct($certificatePEM, self $issuerCertificate = null) /** * @return Certificate[] */ - public function getIssuerChain() + public function getIssuerChain(): array { $chain = []; $issuerCertificate = $this->getIssuerCertificate(); @@ -55,18 +51,12 @@ public function getIssuerChain() return $chain; } - /** - * @return string - */ - public function getPEM() + public function getPEM(): string { return $this->certificatePEM; } - /** - * @return Certificate|null - */ - public function getIssuerCertificate() + public function getIssuerCertificate(): ?self { return $this->issuerCertificate; } @@ -83,10 +73,7 @@ public function getPublicKeyResource() return $resource; } - /** - * @return PublicKey - */ - public function getPublicKey() + public function getPublicKey(): PublicKey { return new PublicKey(openssl_pkey_get_details($this->getPublicKeyResource())['key']); } diff --git a/src/Ssl/CertificateRequest.php b/src/Ssl/CertificateRequest.php index 68a7ab5b..93bea225 100644 --- a/src/Ssl/CertificateRequest.php +++ b/src/Ssl/CertificateRequest.php @@ -30,18 +30,12 @@ public function __construct(DistinguishedName $distinguishedName, KeyPair $keyPa $this->keyPair = $keyPair; } - /** - * @return DistinguishedName - */ - public function getDistinguishedName() + public function getDistinguishedName(): DistinguishedName { return $this->distinguishedName; } - /** - * @return KeyPair - */ - public function getKeyPair() + public function getKeyPair(): KeyPair { return $this->keyPair; } diff --git a/src/Ssl/CertificateResponse.php b/src/Ssl/CertificateResponse.php index 61e2eafd..5767b767 100644 --- a/src/Ssl/CertificateResponse.php +++ b/src/Ssl/CertificateResponse.php @@ -24,26 +24,18 @@ class CertificateResponse /** @var Certificate */ private $certificate; - public function __construct( - CertificateRequest $certificateRequest, - Certificate $certificate - ) { + public function __construct(CertificateRequest $certificateRequest, Certificate $certificate) + { $this->certificateRequest = $certificateRequest; $this->certificate = $certificate; } - /** - * @return CertificateRequest - */ - public function getCertificateRequest() + public function getCertificateRequest(): CertificateRequest { return $this->certificateRequest; } - /** - * @return Certificate - */ - public function getCertificate() + public function getCertificate(): Certificate { return $this->certificate; } diff --git a/src/Ssl/DistinguishedName.php b/src/Ssl/DistinguishedName.php index 824e2fbe..50de2b61 100644 --- a/src/Ssl/DistinguishedName.php +++ b/src/Ssl/DistinguishedName.php @@ -44,32 +44,17 @@ class DistinguishedName /** @var array */ private $subjectAlternativeNames; - /** - * @param string $commonName - * @param string $countryName - * @param string $stateOrProvinceName - * @param string $localityName - * @param string $organizationName - * @param string $organizationalUnitName - * @param string $emailAddress - */ public function __construct( - $commonName, - $countryName = null, - $stateOrProvinceName = null, - $localityName = null, - $organizationName = null, - $organizationalUnitName = null, - $emailAddress = null, + string $commonName, + ?string $countryName = null, + ?string $stateOrProvinceName = null, + ?string $localityName = null, + ?string $organizationName = null, + ?string $organizationalUnitName = null, + ?string $emailAddress = null, array $subjectAlternativeNames = [] ) { Assert::stringNotEmpty($commonName, __CLASS__.'::$commonName expected a non empty string. Got: %s'); - Assert::nullOrStringNotEmpty($countryName, __CLASS__.'::$countryName expected a string. Got: %s'); - Assert::nullOrStringNotEmpty($stateOrProvinceName, __CLASS__.'::$stateOrProvinceName expected a string. Got: %s'); - Assert::nullOrStringNotEmpty($localityName, __CLASS__.'::$localityName expected a string. Got: %s'); - Assert::nullOrStringNotEmpty($organizationName, __CLASS__.'::$organizationName expected a string. Got: %s'); - Assert::nullOrStringNotEmpty($organizationalUnitName, __CLASS__.'::$organizationalUnitName expected a string. Got: %s'); - Assert::nullOrStringNotEmpty($emailAddress, __CLASS__.'::$emailAddress expected a string. Got: %s'); Assert::allStringNotEmpty( $subjectAlternativeNames, __CLASS__.'::$subjectAlternativeNames expected an array of non empty string. Got: %s' @@ -85,66 +70,42 @@ public function __construct( $this->subjectAlternativeNames = array_diff(array_unique($subjectAlternativeNames), [$commonName]); } - /** - * @return string - */ - public function getCommonName() + public function getCommonName(): string { return $this->commonName; } - /** - * @return string - */ - public function getCountryName() + public function getCountryName(): ?string { return $this->countryName; } - /** - * @return string - */ - public function getStateOrProvinceName() + public function getStateOrProvinceName(): ?string { return $this->stateOrProvinceName; } - /** - * @return string - */ - public function getLocalityName() + public function getLocalityName(): ?string { return $this->localityName; } - /** - * @return string - */ - public function getOrganizationName() + public function getOrganizationName(): ?string { return $this->organizationName; } - /** - * @return string - */ - public function getOrganizationalUnitName() + public function getOrganizationalUnitName(): ?string { return $this->organizationalUnitName; } - /** - * @return string - */ - public function getEmailAddress() + public function getEmailAddress(): ?string { return $this->emailAddress; } - /** - * @return array - */ - public function getSubjectAlternativeNames() + public function getSubjectAlternativeNames(): array { return $this->subjectAlternativeNames; } diff --git a/src/Ssl/Generator/ChainPrivateKeyGenerator.php b/src/Ssl/Generator/ChainPrivateKeyGenerator.php index c5d492ea..ffa4df09 100644 --- a/src/Ssl/Generator/ChainPrivateKeyGenerator.php +++ b/src/Ssl/Generator/ChainPrivateKeyGenerator.php @@ -11,6 +11,8 @@ namespace AcmePhp\Ssl\Generator; +use AcmePhp\Ssl\PrivateKey; + /** * Generate random RSA private key using OpenSSL. * @@ -24,12 +26,12 @@ class ChainPrivateKeyGenerator implements PrivateKeyGeneratorInterface /** * @param PrivateKeyGeneratorInterface[] $generators */ - public function __construct($generators) + public function __construct(iterable $generators) { $this->generators = $generators; } - public function generatePrivateKey(KeyOption $keyOption) + public function generatePrivateKey(KeyOption $keyOption): PrivateKey { foreach ($this->generators as $generator) { if ($generator->supportsKeyOption($keyOption)) { @@ -40,7 +42,7 @@ public function generatePrivateKey(KeyOption $keyOption) throw new \LogicException(sprintf('Unable to find a generator for a key option of type %s', \get_class($keyOption))); } - public function supportsKeyOption(KeyOption $keyOption) + public function supportsKeyOption(KeyOption $keyOption): bool { foreach ($this->generators as $generator) { if ($generator->supportsKeyOption($keyOption)) { diff --git a/src/Ssl/Generator/DhKey/DhKeyGenerator.php b/src/Ssl/Generator/DhKey/DhKeyGenerator.php index 4b719e5e..0b73296f 100644 --- a/src/Ssl/Generator/DhKey/DhKeyGenerator.php +++ b/src/Ssl/Generator/DhKey/DhKeyGenerator.php @@ -14,6 +14,7 @@ use AcmePhp\Ssl\Generator\KeyOption; use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; +use AcmePhp\Ssl\PrivateKey; use Webmozart\Assert\Assert; /** @@ -25,25 +26,20 @@ class DhKeyGenerator implements PrivateKeyGeneratorInterface { use OpensslPrivateKeyGeneratorTrait; - /** - * @param DhKeyOption|KeyOption $keyOption - */ - public function generatePrivateKey(KeyOption $keyOption) + public function generatePrivateKey(KeyOption $keyOption): PrivateKey { Assert::isInstanceOf($keyOption, DhKeyOption::class); - return $this->generatePrivateKeyFromOpensslOptions( - [ - 'private_key_type' => OPENSSL_KEYTYPE_DH, - 'dh' => [ - 'p' => $keyOption->getPrime(), - 'g' => $keyOption->getGenerator(), - ], - ] - ); + return $this->generatePrivateKeyFromOpensslOptions([ + 'private_key_type' => OPENSSL_KEYTYPE_DH, + 'dh' => [ + 'p' => $keyOption->getPrime(), + 'g' => $keyOption->getGenerator(), + ], + ]); } - public function supportsKeyOption(KeyOption $keyOption) + public function supportsKeyOption(KeyOption $keyOption): bool { return $keyOption instanceof DhKeyOption; } diff --git a/src/Ssl/Generator/DhKey/DhKeyOption.php b/src/Ssl/Generator/DhKey/DhKeyOption.php index d6bdc116..0ecc1a55 100644 --- a/src/Ssl/Generator/DhKey/DhKeyOption.php +++ b/src/Ssl/Generator/DhKey/DhKeyOption.php @@ -17,6 +17,7 @@ class DhKeyOption implements KeyOption { /** @var string */ private $generator; + /** @var string */ private $prime; @@ -26,24 +27,18 @@ class DhKeyOption implements KeyOption * * @see https://tools.ietf.org/html/rfc3526 how to choose a prime and generator numbers */ - public function __construct($prime, $generator = '02') + public function __construct(string $prime, string $generator = '02') { $this->generator = pack('H*', $generator); $this->prime = pack('H*', $prime); } - /** - * @return string - */ - public function getGenerator() + public function getGenerator(): string { return $this->generator; } - /** - * @return string - */ - public function getPrime() + public function getPrime(): string { return $this->prime; } diff --git a/src/Ssl/Generator/DsaKey/DsaKeyGenerator.php b/src/Ssl/Generator/DsaKey/DsaKeyGenerator.php index 5547b568..7bf15382 100644 --- a/src/Ssl/Generator/DsaKey/DsaKeyGenerator.php +++ b/src/Ssl/Generator/DsaKey/DsaKeyGenerator.php @@ -14,6 +14,7 @@ use AcmePhp\Ssl\Generator\KeyOption; use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; +use AcmePhp\Ssl\PrivateKey; use Webmozart\Assert\Assert; /** @@ -25,22 +26,17 @@ class DsaKeyGenerator implements PrivateKeyGeneratorInterface { use OpensslPrivateKeyGeneratorTrait; - /** - * @param DsaKeyOption|KeyOption $keyOption - */ - public function generatePrivateKey(KeyOption $keyOption) + public function generatePrivateKey(KeyOption $keyOption): PrivateKey { Assert::isInstanceOf($keyOption, DsaKeyOption::class); - return $this->generatePrivateKeyFromOpensslOptions( - [ - 'private_key_type' => OPENSSL_KEYTYPE_DSA, - 'private_key_bits' => $keyOption->getBits(), - ] - ); + return $this->generatePrivateKeyFromOpensslOptions([ + 'private_key_type' => OPENSSL_KEYTYPE_DSA, + 'private_key_bits' => $keyOption->getBits(), + ]); } - public function supportsKeyOption(KeyOption $keyOption) + public function supportsKeyOption(KeyOption $keyOption): bool { return $keyOption instanceof DsaKeyOption; } diff --git a/src/Ssl/Generator/DsaKey/DsaKeyOption.php b/src/Ssl/Generator/DsaKey/DsaKeyOption.php index 344cbabd..615d80e2 100644 --- a/src/Ssl/Generator/DsaKey/DsaKeyOption.php +++ b/src/Ssl/Generator/DsaKey/DsaKeyOption.php @@ -12,24 +12,18 @@ namespace AcmePhp\Ssl\Generator\DsaKey; use AcmePhp\Ssl\Generator\KeyOption; -use Webmozart\Assert\Assert; class DsaKeyOption implements KeyOption { /** @var int */ private $bits; - public function __construct($bits = 2048) + public function __construct(int $bits = 2048) { - Assert::integer($bits); - $this->bits = $bits; } - /** - * @return int - */ - public function getBits() + public function getBits(): int { return $this->bits; } diff --git a/src/Ssl/Generator/EcKey/EcKeyGenerator.php b/src/Ssl/Generator/EcKey/EcKeyGenerator.php index ebcbe6d9..88ebc0c3 100644 --- a/src/Ssl/Generator/EcKey/EcKeyGenerator.php +++ b/src/Ssl/Generator/EcKey/EcKeyGenerator.php @@ -14,6 +14,7 @@ use AcmePhp\Ssl\Generator\KeyOption; use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; +use AcmePhp\Ssl\PrivateKey; use Webmozart\Assert\Assert; /** @@ -25,26 +26,17 @@ class EcKeyGenerator implements PrivateKeyGeneratorInterface { use OpensslPrivateKeyGeneratorTrait; - /** - * @param EcKeyOption|KeyOption $keyOption - */ - public function generatePrivateKey(KeyOption $keyOption) + public function generatePrivateKey(KeyOption $keyOption): PrivateKey { - if (\PHP_VERSION_ID < 70100) { - throw new \LogicException('The generation of ECDSA requires a version of PHP >= 7.1'); - } - Assert::isInstanceOf($keyOption, EcKeyOption::class); - return $this->generatePrivateKeyFromOpensslOptions( - [ - 'private_key_type' => OPENSSL_KEYTYPE_EC, - 'curve_name' => $keyOption->getCurveName(), - ] - ); + return $this->generatePrivateKeyFromOpensslOptions([ + 'private_key_type' => OPENSSL_KEYTYPE_EC, + 'curve_name' => $keyOption->getCurveName(), + ]); } - public function supportsKeyOption(KeyOption $keyOption) + public function supportsKeyOption(KeyOption $keyOption): bool { return $keyOption instanceof EcKeyOption; } diff --git a/src/Ssl/Generator/EcKey/EcKeyOption.php b/src/Ssl/Generator/EcKey/EcKeyOption.php index 813be898..c2747415 100644 --- a/src/Ssl/Generator/EcKey/EcKeyOption.php +++ b/src/Ssl/Generator/EcKey/EcKeyOption.php @@ -19,22 +19,14 @@ class EcKeyOption implements KeyOption /** @var string */ private $curveName; - public function __construct($curveName = 'secp384r1') + public function __construct(string $curveName = 'secp384r1') { - if (\PHP_VERSION_ID < 70100) { - throw new \LogicException('The generation of ECDSA requires a version of PHP >= 7.1'); - } - - Assert::stringNotEmpty($curveName); Assert::oneOf($curveName, openssl_get_curve_names(), 'The given curve %s is not supported. Available curves are: %s'); $this->curveName = $curveName; } - /** - * @return string - */ - public function getCurveName() + public function getCurveName(): string { return $this->curveName; } diff --git a/src/Ssl/Generator/KeyPairGenerator.php b/src/Ssl/Generator/KeyPairGenerator.php index 1e248b85..328be8bb 100644 --- a/src/Ssl/Generator/KeyPairGenerator.php +++ b/src/Ssl/Generator/KeyPairGenerator.php @@ -19,7 +19,6 @@ use AcmePhp\Ssl\Generator\RsaKey\RsaKeyGenerator; use AcmePhp\Ssl\Generator\RsaKey\RsaKeyOption; use AcmePhp\Ssl\KeyPair; -use Webmozart\Assert\Assert; /** * Generate random KeyPair using OpenSSL. @@ -30,7 +29,7 @@ class KeyPairGenerator { private $generator; - public function __construct(PrivateKeyGeneratorInterface $generator = null) + public function __construct(?PrivateKeyGeneratorInterface $generator = null) { $this->generator = $generator ?: new ChainPrivateKeyGenerator( [ @@ -43,24 +42,15 @@ public function __construct(PrivateKeyGeneratorInterface $generator = null) } /** - * Generate KeyPair. - * - * @param KeyOption $keyOption configuration of the key to generate + * @param KeyOption|null $keyOption configuration of the key to generate * * @throws KeyPairGenerationException when OpenSSL failed to generate keys - * - * @return KeyPair */ - public function generateKeyPair($keyOption = null) + public function generateKeyPair(?KeyOption $keyOption = null): KeyPair { if (null === $keyOption) { $keyOption = new RsaKeyOption(); } - if (\is_int($keyOption)) { - @trigger_error('Passing a keySize to "generateKeyPair" is deprecated since version 1.1 and will be removed in 2.0. Pass an instance of KeyOption instead', E_USER_DEPRECATED); - $keyOption = new RsaKeyOption($keyOption); - } - Assert::isInstanceOf($keyOption, KeyOption::class); try { $privateKey = $this->generator->generatePrivateKey($keyOption); diff --git a/src/Ssl/Generator/OpensslPrivateKeyGeneratorTrait.php b/src/Ssl/Generator/OpensslPrivateKeyGeneratorTrait.php index 31677586..7fc1e3df 100644 --- a/src/Ssl/Generator/OpensslPrivateKeyGeneratorTrait.php +++ b/src/Ssl/Generator/OpensslPrivateKeyGeneratorTrait.php @@ -17,7 +17,7 @@ trait OpensslPrivateKeyGeneratorTrait { - private function generatePrivateKeyFromOpensslOptions(array $opensslOptions) + private function generatePrivateKeyFromOpensslOptions(array $opensslOptions): PrivateKey { $resource = openssl_pkey_new($opensslOptions); @@ -28,7 +28,10 @@ private function generatePrivateKeyFromOpensslOptions(array $opensslOptions) throw new KeyPairGenerationException(sprintf('OpenSSL key export failed during generation with error: %s', openssl_error_string())); } - openssl_free_key($resource); + // PHP 8 automatically frees the key instance and deprecates the function + if (\PHP_VERSION_ID < 80000) { + openssl_free_key($resource); + } return new PrivateKey($privateKey); } diff --git a/src/Ssl/Generator/PrivateKeyGeneratorInterface.php b/src/Ssl/Generator/PrivateKeyGeneratorInterface.php index e0ea2086..98860251 100644 --- a/src/Ssl/Generator/PrivateKeyGeneratorInterface.php +++ b/src/Ssl/Generator/PrivateKeyGeneratorInterface.php @@ -27,17 +27,13 @@ interface PrivateKeyGeneratorInterface * @param KeyOption $keyOption configuration of the key to generate * * @throws KeyGenerationException when OpenSSL failed to generate keys - * - * @return PrivateKey */ - public function generatePrivateKey(KeyOption $keyOption); + public function generatePrivateKey(KeyOption $keyOption): PrivateKey; /** * Returns whether the instance is able to generator a private key from the given option. * * @param KeyOption $keyOption configuration of the key to generate - * - * @return bool */ - public function supportsKeyOption(KeyOption $keyOption); + public function supportsKeyOption(KeyOption $keyOption): bool; } diff --git a/src/Ssl/Generator/RsaKey/RsaKeyGenerator.php b/src/Ssl/Generator/RsaKey/RsaKeyGenerator.php index 78f356fe..6d85a248 100644 --- a/src/Ssl/Generator/RsaKey/RsaKeyGenerator.php +++ b/src/Ssl/Generator/RsaKey/RsaKeyGenerator.php @@ -14,6 +14,7 @@ use AcmePhp\Ssl\Generator\KeyOption; use AcmePhp\Ssl\Generator\OpensslPrivateKeyGeneratorTrait; use AcmePhp\Ssl\Generator\PrivateKeyGeneratorInterface; +use AcmePhp\Ssl\PrivateKey; use Webmozart\Assert\Assert; /** @@ -25,22 +26,17 @@ class RsaKeyGenerator implements PrivateKeyGeneratorInterface { use OpensslPrivateKeyGeneratorTrait; - /** - * @param RsaKeyOption|KeyOption $keyOption - */ - public function generatePrivateKey(KeyOption $keyOption) + public function generatePrivateKey(KeyOption $keyOption): PrivateKey { Assert::isInstanceOf($keyOption, RsaKeyOption::class); - return $this->generatePrivateKeyFromOpensslOptions( - [ - 'private_key_type' => OPENSSL_KEYTYPE_RSA, - 'private_key_bits' => $keyOption->getBits(), - ] - ); + return $this->generatePrivateKeyFromOpensslOptions([ + 'private_key_type' => OPENSSL_KEYTYPE_RSA, + 'private_key_bits' => $keyOption->getBits(), + ]); } - public function supportsKeyOption(KeyOption $keyOption) + public function supportsKeyOption(KeyOption $keyOption): bool { return $keyOption instanceof RsaKeyOption; } diff --git a/src/Ssl/Generator/RsaKey/RsaKeyOption.php b/src/Ssl/Generator/RsaKey/RsaKeyOption.php index 27f9345d..1c8e6f50 100644 --- a/src/Ssl/Generator/RsaKey/RsaKeyOption.php +++ b/src/Ssl/Generator/RsaKey/RsaKeyOption.php @@ -12,24 +12,18 @@ namespace AcmePhp\Ssl\Generator\RsaKey; use AcmePhp\Ssl\Generator\KeyOption; -use Webmozart\Assert\Assert; class RsaKeyOption implements KeyOption { /** @var int */ private $bits; - public function __construct($bits = 4096) + public function __construct(int $bits = 4096) { - Assert::integer($bits); - $this->bits = $bits; } - /** - * @return int - */ - public function getBits() + public function getBits(): int { return $this->bits; } diff --git a/src/Ssl/Key.php b/src/Ssl/Key.php index 1249b546..6852ce24 100644 --- a/src/Ssl/Key.php +++ b/src/Ssl/Key.php @@ -23,28 +23,19 @@ abstract class Key /** @var string */ protected $keyPEM; - /** - * @param string $keyPEM - */ - public function __construct($keyPEM) + public function __construct(string $keyPEM) { Assert::stringNotEmpty($keyPEM, __CLASS__.'::$keyPEM should not be an empty string. Got %s'); $this->keyPEM = $keyPEM; } - /** - * @return string - */ - public function getPEM() + public function getPEM(): string { return $this->keyPEM; } - /** - * @return string - */ - public function getDER() + public function getDER(): string { $lines = explode("\n", trim($this->keyPEM)); unset($lines[\count($lines) - 1]); @@ -56,7 +47,7 @@ public function getDER() } /** - * @return resource + * @return resource|\OpenSSLAsymmetricKey */ abstract public function getResource(); } diff --git a/src/Ssl/KeyPair.php b/src/Ssl/KeyPair.php index 6d733c6a..903d65ff 100644 --- a/src/Ssl/KeyPair.php +++ b/src/Ssl/KeyPair.php @@ -30,18 +30,12 @@ public function __construct(PublicKey $publicKey, PrivateKey $privateKey) $this->privateKey = $privateKey; } - /** - * @return PublicKey - */ - public function getPublicKey() + public function getPublicKey(): PublicKey { return $this->publicKey; } - /** - * @return PrivateKey - */ - public function getPrivateKey() + public function getPrivateKey(): PrivateKey { return $this->privateKey; } diff --git a/src/Ssl/ParsedCertificate.php b/src/Ssl/ParsedCertificate.php index c2770f21..ef8c21a0 100644 --- a/src/Ssl/ParsedCertificate.php +++ b/src/Ssl/ParsedCertificate.php @@ -44,28 +44,17 @@ class ParsedCertificate /** @var array */ private $subjectAlternativeNames; - /** - * @param string $subject - * @param string $issuer - * @param bool $selfSigned - * @param \DateTime $validFrom - * @param \DateTime $validTo - * @param string $serialNumber - */ public function __construct( Certificate $source, - $subject, - $issuer = null, - $selfSigned = true, - \DateTime $validFrom = null, - \DateTime $validTo = null, - $serialNumber = null, + string $subject, + ?string $issuer = null, + bool $selfSigned = true, + ?\DateTime $validFrom = null, + ?\DateTime $validTo = null, + ?string $serialNumber = null, array $subjectAlternativeNames = [] ) { Assert::stringNotEmpty($subject, __CLASS__.'::$subject expected a non empty string. Got: %s'); - Assert::nullOrString($issuer, __CLASS__.'::$issuer expected a string or null. Got: %s'); - Assert::nullOrBoolean($selfSigned, __CLASS__.'::$selfSigned expected a boolean or null. Got: %s'); - Assert::nullOrString($serialNumber, __CLASS__.'::$serialNumber expected a string or null. Got: %s'); Assert::allStringNotEmpty( $subjectAlternativeNames, __CLASS__.'::$subjectAlternativeNames expected a array of non empty string. Got: %s' @@ -81,74 +70,47 @@ public function __construct( $this->subjectAlternativeNames = $subjectAlternativeNames; } - /** - * @return Certificate - */ - public function getSource() + public function getSource(): Certificate { return $this->source; } - /** - * @return string - */ - public function getSubject() + public function getSubject(): string { return $this->subject; } - /** - * @return string - */ - public function getIssuer() + public function getIssuer(): ?string { return $this->issuer; } - /** - * @return bool - */ - public function isSelfSigned() + public function isSelfSigned(): bool { return $this->selfSigned; } - /** - * @return \DateTime - */ - public function getValidFrom() + public function getValidFrom(): \DateTimeInterface { return $this->validFrom; } - /** - * @return \DateTime - */ - public function getValidTo() + public function getValidTo(): \DateTimeInterface { return $this->validTo; } - /** - * @return bool - */ - public function isExpired() + public function isExpired(): bool { return $this->validTo < (new \DateTime()); } - /** - * @return string - */ - public function getSerialNumber() + public function getSerialNumber(): ?string { return $this->serialNumber; } - /** - * @return array - */ - public function getSubjectAlternativeNames() + public function getSubjectAlternativeNames(): array { return $this->subjectAlternativeNames; } diff --git a/src/Ssl/ParsedKey.php b/src/Ssl/ParsedKey.php index 083013f9..4a706246 100644 --- a/src/Ssl/ParsedKey.php +++ b/src/Ssl/ParsedKey.php @@ -37,15 +37,9 @@ class ParsedKey /** @var array */ private $details; - /** - * @param string $key - * @param int $bits - * @param int $type - */ - public function __construct(Key $source, $key, $bits, $type, array $details = []) + public function __construct(Key $source, string $key, int $bits, int $type, array $details = []) { Assert::stringNotEmpty($key, __CLASS__.'::$key expected a non empty string. Got: %s'); - Assert::integer($bits, __CLASS__.'::$bits expected an integer. Got: %s'); Assert::oneOf( $type, [OPENSSL_KEYTYPE_RSA, OPENSSL_KEYTYPE_DSA, OPENSSL_KEYTYPE_DH, OPENSSL_KEYTYPE_EC], @@ -59,62 +53,37 @@ public function __construct(Key $source, $key, $bits, $type, array $details = [] $this->details = $details; } - /** - * @return Key - */ - public function getSource() + public function getSource(): Key { return $this->source; } - /** - * @return string - */ - public function getKey() + public function getKey(): string { return $this->key; } - /** - * @return int - */ - public function getBits() + public function getBits(): int { return $this->bits; } - /** - * @return int - */ - public function getType() + public function getType(): int { return $this->type; } - /** - * @return array - */ - public function getDetails() + public function getDetails(): array { return $this->details; } - /** - * @param string $name - * - * @return bool - */ - public function hasDetail($name) + public function hasDetail(string $name): bool { return isset($this->details[$name]); } - /** - * @param string $name - * - * @return mixed - */ - public function getDetail($name) + public function getDetail(string $name) { Assert::oneOf($name, array_keys($this->details), 'ParsedKey::getDetail() expected one of: %2$s. Got: %s'); diff --git a/src/Ssl/Parser/CertificateParser.php b/src/Ssl/Parser/CertificateParser.php index c01f779a..d3116de9 100644 --- a/src/Ssl/Parser/CertificateParser.php +++ b/src/Ssl/Parser/CertificateParser.php @@ -22,12 +22,7 @@ */ class CertificateParser { - /** - * Parse the certificate. - * - * @return ParsedCertificate - */ - public function parse(Certificate $certificate) + public function parse(Certificate $certificate): ParsedCertificate { $rawData = openssl_x509_parse($certificate->getPEM()); diff --git a/src/Ssl/Parser/KeyParser.php b/src/Ssl/Parser/KeyParser.php index 423074d8..215a975f 100644 --- a/src/Ssl/Parser/KeyParser.php +++ b/src/Ssl/Parser/KeyParser.php @@ -23,12 +23,7 @@ */ class KeyParser { - /** - * Parse the key. - * - * @return ParsedKey - */ - public function parse(Key $key) + public function parse(Key $key): ParsedKey { try { $resource = $key->getResource(); @@ -37,7 +32,11 @@ public function parse(Key $key) } $rawData = openssl_pkey_get_details($resource); - openssl_free_key($resource); + + // PHP 8 automatically frees the key instance and deprecates the function + if (\PHP_VERSION_ID < 80000) { + openssl_free_key($resource); + } if (!\is_array($rawData)) { throw new KeyParsingException(sprintf('Fail to parse key with error: %s', openssl_error_string())); diff --git a/src/Ssl/PrivateKey.php b/src/Ssl/PrivateKey.php index 6bfe96d2..b4508089 100644 --- a/src/Ssl/PrivateKey.php +++ b/src/Ssl/PrivateKey.php @@ -21,9 +21,6 @@ */ class PrivateKey extends Key { - /** - * {@inheritdoc} - */ public function getResource() { if (!$resource = openssl_pkey_get_private($this->keyPEM)) { @@ -33,27 +30,22 @@ public function getResource() return $resource; } - /** - * @return PublicKey - */ - public function getPublicKey() + public function getPublicKey(): PublicKey { $resource = $this->getResource(); if (!$details = openssl_pkey_get_details($resource)) { throw new KeyFormatException(sprintf('Failed to extract public key: %s', openssl_error_string())); } - openssl_free_key($resource); + // PHP 8 automatically frees the key instance and deprecates the function + if (\PHP_VERSION_ID < 80000) { + openssl_free_key($resource); + } return new PublicKey($details['key']); } - /** - * @param $keyDER - * - * @return PrivateKey - */ - public static function fromDER($keyDER) + public static function fromDER(string $keyDER): self { Assert::stringNotEmpty($keyDER, __METHOD__.'::$keyDER should be a non-empty string. Got %s'); diff --git a/src/Ssl/PublicKey.php b/src/Ssl/PublicKey.php index 83040c29..aa570c62 100644 --- a/src/Ssl/PublicKey.php +++ b/src/Ssl/PublicKey.php @@ -21,9 +21,6 @@ */ class PublicKey extends Key { - /** - * {@inheritdoc} - */ public function getResource() { if (!$resource = openssl_pkey_get_public($this->keyPEM)) { @@ -33,12 +30,7 @@ public function getResource() return $resource; } - /** - * @param $keyDER - * - * @return PublicKey - */ - public static function fromDER($keyDER) + public static function fromDER(string $keyDER): self { Assert::stringNotEmpty($keyDER, __METHOD__.'::$keyDER should be a non-empty string. Got %s'); @@ -51,10 +43,7 @@ public static function fromDER($keyDER) return new self(implode("\n", $lines)); } - /** - * @return string - */ - public function getHPKP() + public function getHPKP(): string { return base64_encode(hash('sha256', $this->getDER(), true)); } diff --git a/src/Ssl/Signer/CertificateRequestSigner.php b/src/Ssl/Signer/CertificateRequestSigner.php index 3c6f4a90..cf7f35cd 100644 --- a/src/Ssl/Signer/CertificateRequestSigner.php +++ b/src/Ssl/Signer/CertificateRequestSigner.php @@ -24,10 +24,8 @@ class CertificateRequestSigner { /** * Generate a CSR from the given distinguishedName and keyPair. - * - * @return string */ - public function signCertificateRequest(CertificateRequest $certificateRequest) + public function signCertificateRequest(CertificateRequest $certificateRequest): string { $csrObject = $this->createCsrWithSANsObject($certificateRequest); @@ -40,8 +38,6 @@ public function signCertificateRequest(CertificateRequest $certificateRequest) /** * Generate a CSR object with SANs from the given distinguishedName and keyPair. - * - * @return mixed */ protected function createCsrWithSANsObject(CertificateRequest $certificateRequest) { @@ -86,7 +82,10 @@ protected function createCsrWithSANsObject(CertificateRequest $certificateReques ] ); - openssl_free_key($resource); + // PHP 8 automatically frees the key instance and deprecates the function + if (\PHP_VERSION_ID < 80000) { + openssl_free_key($resource); + } if (!$csr) { throw new CSRSigningException(sprintf('OpenSSL CSR signing failed with error: %s', openssl_error_string())); @@ -100,10 +99,8 @@ protected function createCsrWithSANsObject(CertificateRequest $certificateReques /** * Retrieves a CSR payload from the given distinguished name. - * - * @return array */ - private function getCSRPayload(DistinguishedName $distinguishedName) + private function getCSRPayload(DistinguishedName $distinguishedName): array { $payload = []; if (null !== $countryName = $distinguishedName->getCountryName()) { diff --git a/src/Ssl/Signer/DataSigner.php b/src/Ssl/Signer/DataSigner.php index 13c0c590..2db48f9e 100644 --- a/src/Ssl/Signer/DataSigner.php +++ b/src/Ssl/Signer/DataSigner.php @@ -22,8 +22,8 @@ */ class DataSigner { - const FORMAT_DER = 'DER'; - const FORMAT_ECDSA = 'ECDSA'; + public const FORMAT_DER = 'DER'; + public const FORMAT_ECDSA = 'ECDSA'; /** * Generate a signature of the given data using a private key and an algorithm. @@ -32,10 +32,8 @@ class DataSigner * @param PrivateKey $privateKey Key used to sign * @param int $algorithm Signature algorithm defined by constants OPENSSL_ALGO_* * @param string $format Format of the output - * - * @return string */ - public function signData($data, PrivateKey $privateKey, $algorithm = OPENSSL_ALGO_SHA256, $format = self::FORMAT_DER) + public function signData(string $data, PrivateKey $privateKey, int $algorithm = OPENSSL_ALGO_SHA256, string $format = self::FORMAT_DER): string { Assert::oneOf($format, [self::FORMAT_ECDSA, self::FORMAT_DER], 'The format %s to sign request does not exists. Available format: %s'); @@ -44,7 +42,10 @@ public function signData($data, PrivateKey $privateKey, $algorithm = OPENSSL_ALG throw new DataSigningException(sprintf('OpenSSL data signing failed with error: %s', openssl_error_string())); } - openssl_free_key($resource); + // PHP 8 automatically frees the key instance and deprecates the function + if (\PHP_VERSION_ID < 80000) { + openssl_free_key($resource); + } switch ($format) { case self::FORMAT_DER: @@ -71,7 +72,7 @@ public function signData($data, PrivateKey $privateKey, $algorithm = OPENSSL_ALG * * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php */ - private function DERtoECDSA($der, $partLength) + private function DERtoECDSA($der, $partLength): string { $hex = unpack('H*', $der)[1]; if ('30' !== mb_substr($hex, 0, 2, '8bit')) { // SEQUENCE @@ -101,23 +102,6 @@ private function DERtoECDSA($der, $partLength) return pack('H*', $R.$S); } - /** - * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0. - * - * @see https://github.com/web-token/jwt-core/blob/master/Util/ECSignature.php - */ - private function preparePositiveInteger($data) - { - if (mb_substr($data, 0, 2, '8bit') > '7f') { - return '00'.$data; - } - while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') <= '7f') { - $data = mb_substr($data, 2, null, '8bit'); - } - - return $data; - } - /** * The code is a copy/paste from another lib (web-token/jwt-core) which is not compatible with php <= 7.0. * diff --git a/src/Ssl/composer.json b/src/Ssl/composer.json index 017da12f..57b41d61 100644 --- a/src/Ssl/composer.json +++ b/src/Ssl/composer.json @@ -32,18 +32,13 @@ } }, "require": { - "php": ">=5.5.0", + "php": ">=8.3", "ext-hash": "*", "ext-openssl": "*", "lib-openssl": ">=0.9.8", "webmozart/assert": "^1.0" }, - "require-dev": { - "phpunit/phpunit": "^4.8.22" - }, - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } + "config": { + "sort-packages": true } } diff --git a/tests/Cli/AbstractApplicationTest.php b/tests/Cli/AbstractApplicationTest.php index b8f164d4..62ebf8ae 100644 --- a/tests/Cli/AbstractApplicationTest.php +++ b/tests/Cli/AbstractApplicationTest.php @@ -14,126 +14,64 @@ use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; -use Tests\AcmePhp\Cli\Mock\AbstractTestApplication; -use Tests\AcmePhp\Cli\Mock\SimpleApplication; +use Tests\AcmePhp\Cli\Mock\TestApplication; use Tests\AcmePhp\Core\AbstractFunctionnalTest; -use Webmozart\PathUtil\Path; abstract class AbstractApplicationTest extends AbstractFunctionnalTest { /** - * @var AbstractTestApplication + * @var TestApplication */ protected $application; - /** - * @return array - */ - abstract protected function getFixturesDirectories(); + abstract protected function getFixturesDirectories(): array; - /** - * @return AbstractTestApplication - */ - abstract protected function createApplication(); + abstract protected function getConfigDir(): string; - public function setUp() + public function setUp(): void { $this->cleanContext(); - $this->application = $this->createApplication(); + $this->application = new TestApplication(); } - public function tearDown() + public function tearDown(): void { $this->cleanContext(); } public function testFullProcess() { - /* - * Register - */ - $register = $this->application->find('register'); - $registerTester = new CommandTester($register); - $registerTester->execute([ - 'command' => $register->getName(), - 'email' => 'foo@example.com', - '--server' => 'https://localhost:14000/dir', - ]); - - $registerDisplay = $registerTester->getDisplay(); - - $this->assertStringContainsString('No account key pair was found, generating one', $registerDisplay); - $this->assertStringContainsString('Account registered successfully', $registerDisplay); + $runTester = new CommandTester($this->application->find('run')); + $runTester->execute( + [ + 'command' => 'run', + 'config' => $this->getConfigDir().'/'.('eab' === getenv('PEBBLE_MODE') ? 'eab' : 'default').'.yaml', + ], + [ + 'decorated' => true, + ] + ); + + $output = $runTester->getDisplay(); + + $this->assertStringContainsString("\e[32mLoading account key pair... \e[39m", $output); + + // Register + $this->assertStringContainsString('No account key pair was found, generating one', $output); + $this->assertStringContainsString('Account registered successfully', $output); $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/account/key.private.pem'); $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/account/key.public.pem'); - /* - * Authorize - */ - $authorize = $this->application->find('authorize'); - $authorizeTest = new CommandTester($authorize); - $authorizeTest->execute([ - 'command' => $authorize->getName(), - 'domains' => ['acmephp.com'], - '--server' => 'https://localhost:14000/dir', - ]); - - $authorizeDisplay = $authorizeTest->getDisplay(); - - $this->assertStringContainsString('The authorization tokens was successfully fetched', $authorizeDisplay); - $this->assertStringContainsString('http://acmephp.com/.well-known/acme-challenge/', $authorizeDisplay); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/var/acmephp.com/authorization_challenge.json'); - - /* - * Check - */ - - // Find challenge and expose token - $challenge = json_decode( - file_get_contents(__DIR__.'/../Cli/Fixtures/local/master/var/acmephp.com/authorization_challenge.json'), - true - ); + // Challenge + $this->assertStringContainsString('Requesting certificate order', $output); + $this->assertStringContainsString('Solving challenge for domain acmephp.com', $output); + $this->assertStringContainsString('Requesting authorization check for domain acmephp.com', $output); + $this->assertStringContainsString('Cleaning up challenge for domain acmephp.com', $output); - $this->handleChallenge($challenge['token'], $challenge['payload']); - try { - $check = $this->application->find('check'); - $checkTest = new CommandTester($check); - $checkTest->execute([ - 'command' => $check->getName(), - 'domains' => ['acmephp.com'], - '--server' => 'https://localhost:14000/dir', - '--no-test' => null, - ]); - - $checkDisplay = $checkTest->getDisplay(); - - $this->assertStringContainsString('The authorization check was successful', $checkDisplay); - } finally { - $this->cleanChallenge($challenge['token']); - } - - /* - * Request - */ - $request = $this->application->find('request'); - $requestTest = new CommandTester($request); - $requestTest->execute([ - 'command' => $request->getName(), - 'domain' => 'acmephp.com', - '--server' => 'https://localhost:14000/dir', - '--country' => 'FR', - '--province' => 'Ile de France', - '--locality' => 'Paris', - '--organization' => 'Acme PHP', - '--unit' => 'Sales', - '--email' => 'example@acmephp.github.io', - ]); - - $requestDisplay = $requestTest->getDisplay(); - - $this->assertStringContainsString('The SSL certificate was fetched successfully', $requestDisplay); - $this->assertStringContainsString(Path::canonicalize(__DIR__.'/Fixtures/local/master'), $requestDisplay); + // Certificate + $this->assertStringContainsString('Requesting certificate for domain acmephp.com', $output); + $this->assertStringContainsString('Certificate requested successfully!', $output); $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/key.private.pem'); $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/key.public.pem'); $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/cert.pem'); @@ -142,54 +80,6 @@ public function testFullProcess() $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/fullchain.pem'); } - public function testCheckWithoutKeyFail() - { - $this->expectException('AcmePhp\Cli\Exception\CommandFlowException'); - $this->application = new SimpleApplication(); - - $command = $this->application->find('check'); - $commandTester = new CommandTester($command); - $commandTester->execute([ - 'command' => $command->getName(), - 'domains' => ['example.com'], - '--server' => 'https://localhost:14000/dir', - ]); - } - - public function testAuthorizeWithoutKeyFail() - { - $this->expectException('AcmePhp\Cli\Exception\CommandFlowException'); - $this->application = new SimpleApplication(); - - $command = $this->application->find('authorize'); - $commandTester = new CommandTester($command); - $commandTester->execute([ - 'command' => $command->getName(), - 'domains' => 'example.com', - '--server' => 'https://localhost:14000/dir', - ]); - } - - public function testRequestWithoutKeyFail() - { - $this->expectException('AcmePhp\Cli\Exception\CommandFlowException'); - $this->application = new SimpleApplication(); - - $command = $this->application->find('request'); - $commandTester = new CommandTester($command); - $commandTester->execute([ - 'command' => $command->getName(), - 'domain' => 'acmephp.com', - '--server' => 'https://localhost:14000/dir', - '--country' => 'FR', - '--province' => 'Ile de France', - '--locality' => 'Paris', - '--organization' => 'Acme PHP', - '--unit' => 'Sales', - '--email' => 'example@acmephp.github.io', - ]); - } - /** * Remove fixtures files and directories to have a clean context. */ diff --git a/tests/Cli/Action/InstallAwsElbActionTest.php b/tests/Cli/Action/InstallAwsElbActionTest.php index c507d9b1..2e551f9d 100644 --- a/tests/Cli/Action/InstallAwsElbActionTest.php +++ b/tests/Cli/Action/InstallAwsElbActionTest.php @@ -24,9 +24,12 @@ use Aws\Iam\IamClient; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; class InstallAwsElbActionTest extends TestCase { + use ProphecyTrait; + public function testHandle() { $domain = 'foo.bar'; diff --git a/tests/Cli/Action/InstallAwsElbv2ActionTest.php b/tests/Cli/Action/InstallAwsElbv2ActionTest.php index 1a4d6928..38fa7eb9 100644 --- a/tests/Cli/Action/InstallAwsElbv2ActionTest.php +++ b/tests/Cli/Action/InstallAwsElbv2ActionTest.php @@ -24,9 +24,12 @@ use Aws\Iam\IamClient; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; class InstallAwsElbv2ActionTest extends TestCase { + use ProphecyTrait; + public function testHandle() { $domain = 'foo.bar'; diff --git a/tests/Cli/Fixtures/config/sfpt_nginxproxy/default.yaml b/tests/Cli/Fixtures/config/sfpt_nginxproxy/default.yaml new file mode 100644 index 00000000..7a6274a5 --- /dev/null +++ b/tests/Cli/Fixtures/config/sfpt_nginxproxy/default.yaml @@ -0,0 +1,23 @@ +contact_email: foo@example.com +key_type: RSA +provider: localhost + +defaults: + distinguished_name: + country: FR + locality: Paris + organization_name: Acme PHP + +certificates: + - domain: acmephp.com + solver: + name: mock-server + install: + - action: build_nginxproxy + - action: mirror_file + adapter: sftp + root: /share + host: localhost + username: acmephp + password: acmephp + port: 8022 diff --git a/tests/Cli/Fixtures/config/sfpt_nginxproxy/eab.yaml b/tests/Cli/Fixtures/config/sfpt_nginxproxy/eab.yaml new file mode 100644 index 00000000..cfc6dfca --- /dev/null +++ b/tests/Cli/Fixtures/config/sfpt_nginxproxy/eab.yaml @@ -0,0 +1,25 @@ +contact_email: foo@example.com +key_type: RSA +provider: localhost +eab_kid: kid1 +eab_hmac_key: dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5n + +defaults: + distinguished_name: + country: FR + locality: Paris + organization_name: Acme PHP + +certificates: + - domain: acmephp.com + solver: + name: mock-server + install: + - action: build_nginxproxy + - action: mirror_file + adapter: sftp + root: /share + host: localhost + username: acmephp + password: acmephp + port: 8022 diff --git a/tests/Cli/Fixtures/config/simple/default.yaml b/tests/Cli/Fixtures/config/simple/default.yaml new file mode 100644 index 00000000..b814b95c --- /dev/null +++ b/tests/Cli/Fixtures/config/simple/default.yaml @@ -0,0 +1,14 @@ +contact_email: foo@example.com +key_type: RSA +provider: localhost + +defaults: + distinguished_name: + country: FR + locality: Paris + organization_name: Acme PHP + +certificates: + - domain: acmephp.com + solver: + name: mock-server diff --git a/tests/Cli/Fixtures/config/simple/eab.yaml b/tests/Cli/Fixtures/config/simple/eab.yaml new file mode 100644 index 00000000..6d8ee8ce --- /dev/null +++ b/tests/Cli/Fixtures/config/simple/eab.yaml @@ -0,0 +1,16 @@ +contact_email: foo@example.com +key_type: RSA +provider: localhost +eab_kid: kid1 +eab_hmac_key: dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5n + +defaults: + distinguished_name: + country: FR + locality: Paris + organization_name: Acme PHP + +certificates: + - domain: acmephp.com + solver: + name: mock-server diff --git a/tests/Cli/Fixtures/monitoring.conf b/tests/Cli/Fixtures/monitoring.conf deleted file mode 100644 index fd24caa0..00000000 --- a/tests/Cli/Fixtures/monitoring.conf +++ /dev/null @@ -1,20 +0,0 @@ -storage: - enable_backup: false - post_generate: ~ - -monitoring: - email: - to: galopintitouan@gmail.com - host: smtp.example.com - port: 25 - username: foo - password: bar - encryption: tls - subject: foo_subject - level: info - - slack: - token: foo_token - channel: foochannel - username: Acme PHP - level: error diff --git a/tests/Cli/Fixtures/sftp_nginxproxy.conf b/tests/Cli/Fixtures/sftp_nginxproxy.conf deleted file mode 100644 index 66bab04b..00000000 --- a/tests/Cli/Fixtures/sftp_nginxproxy.conf +++ /dev/null @@ -1,13 +0,0 @@ -storage: - enable_backup: true - post_generate: - - action: build_nginxproxy - - action: mirror_file - adapter: sftp - root: /share - host: localhost - username: acmephp - password: acmephp - port: 8022 - -monitoring: ~ diff --git a/tests/Cli/Fixtures/simple.conf b/tests/Cli/Fixtures/simple.conf deleted file mode 100644 index 319c6875..00000000 --- a/tests/Cli/Fixtures/simple.conf +++ /dev/null @@ -1,5 +0,0 @@ -storage: - enable_backup: false - post_generate: ~ - -monitoring: ~ diff --git a/tests/Cli/Mock/MonitoredApplication.php b/tests/Cli/Mock/MonitoredApplication.php deleted file mode 100644 index c7828e8d..00000000 --- a/tests/Cli/Mock/MonitoredApplication.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\AcmePhp\Cli\Mock; - -use Webmozart\PathUtil\Path; - -class MonitoredApplication extends AbstractTestApplication -{ - /** - * @return string - */ - public function getConfigFile() - { - return Path::canonicalize(__DIR__.'/../Fixtures/monitoring.conf'); - } -} diff --git a/tests/Cli/Mock/SftpNginxProxyApplication.php b/tests/Cli/Mock/SftpNginxProxyApplication.php deleted file mode 100644 index 5f600105..00000000 --- a/tests/Cli/Mock/SftpNginxProxyApplication.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\AcmePhp\Cli\Mock; - -use Webmozart\PathUtil\Path; - -class SftpNginxProxyApplication extends AbstractTestApplication -{ - /** - * @return string - */ - public function getConfigFile() - { - return Path::canonicalize(__DIR__.'/../Fixtures/sftp_nginxproxy.conf'); - } -} diff --git a/tests/Cli/Mock/SimpleApplication.php b/tests/Cli/Mock/SimpleApplication.php deleted file mode 100644 index 26a26de7..00000000 --- a/tests/Cli/Mock/SimpleApplication.php +++ /dev/null @@ -1,25 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\AcmePhp\Cli\Mock; - -use Webmozart\PathUtil\Path; - -class SimpleApplication extends AbstractTestApplication -{ - /** - * @return string - */ - public function getConfigFile() - { - return Path::canonicalize(__DIR__.'/../Fixtures/simple.conf'); - } -} diff --git a/tests/Cli/Mock/AbstractTestApplication.php b/tests/Cli/Mock/TestApplication.php similarity index 62% rename from tests/Cli/Mock/AbstractTestApplication.php rename to tests/Cli/Mock/TestApplication.php index 0be580b4..4ab3234b 100644 --- a/tests/Cli/Mock/AbstractTestApplication.php +++ b/tests/Cli/Mock/TestApplication.php @@ -11,9 +11,9 @@ namespace Tests\AcmePhp\Cli\Mock; -use Webmozart\PathUtil\Path; +use Symfony\Component\Filesystem\Path; -abstract class AbstractTestApplication extends \AcmePhp\Cli\Application +class TestApplication extends \AcmePhp\Cli\Application { /** * @return string @@ -22,12 +22,4 @@ public function getStorageDirectory() { return Path::canonicalize(__DIR__.'/../Fixtures/local/master'); } - - /** - * @return string - */ - public function getBackupDirectory() - { - return Path::canonicalize(__DIR__.'/../Fixtures/local/backup'); - } } diff --git a/tests/Cli/MonitoredApplicationTest.php b/tests/Cli/MonitoredApplicationTest.php deleted file mode 100644 index 4ed65a55..00000000 --- a/tests/Cli/MonitoredApplicationTest.php +++ /dev/null @@ -1,177 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\AcmePhp\Cli; - -use AcmePhp\Cli\Command\AbstractCommand; -use AcmePhp\Cli\Monitoring\HandlerBuilderInterface; -use AcmePhp\Core\Exception\AcmeCoreClientException; -use GuzzleHttp\Client; -use GuzzleHttp\Exception\RequestException; -use GuzzleHttp\Handler\MockHandler; -use GuzzleHttp\Psr7\Request; -use Monolog\Handler\TestHandler; -use Monolog\Logger; -use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\DependencyInjection\ContainerInterface; -use Tests\AcmePhp\Cli\Mock\AbstractTestApplication; -use Tests\AcmePhp\Cli\Mock\MonitoredApplication; - -class MonitoredApplicationTest extends AbstractApplicationTest -{ - /** - * @return array - */ - protected function getFixturesDirectories() - { - return [ - __DIR__.'/../Cli/Fixtures/local/backup', - __DIR__.'/../Cli/Fixtures/local/master', - ]; - } - - /** - * @return AbstractTestApplication - */ - protected function createApplication() - { - return new MonitoredApplication(); - } - - public function testFullProcess() - { - parent::testFullProcess(); - - /* - * Renewal without issue - */ - $command = $this->application->find('request'); - $commandTester = new CommandTester($command); - $commandTester->execute([ - 'command' => $command->getName(), - 'domain' => 'acmephp.com', - '--server' => 'https://localhost:14000/dir', - '--country' => 'FR', - '--province' => 'Ile de France', - '--locality' => 'Paris', - '--organization' => 'Acme PHP', - '--unit' => 'Sales', - '--email' => 'galopintitouan@gmail.com', - '--force' => true, - ]); - - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/key.private.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/key.public.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/cert.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/combined.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/chain.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/fullchain.pem'); - } - - public function testRenewalWithIssue() - { - parent::testFullProcess(); - - $command = $this->application->find('request'); - $commandTester = new CommandTester($command); - $commandTester->execute([ - 'command' => $command->getName(), - 'domain' => 'acmephp.com', - '--server' => 'https://localhost:14000/dir', - '--country' => 'FR', - '--province' => 'Ile de France', - '--locality' => 'Paris', - '--organization' => 'Acme PHP', - '--unit' => 'Sales', - '--email' => 'galopintitouan@gmail.com', - '--force' => true, - ]); - - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/key.private.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/key.public.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/cert.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/private/combined.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/chain.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/certs/acmephp.com/public/fullchain.pem'); - - /* - * Mock monitoring handlers - */ - - // Initialize container - $parentReflection = new \ReflectionClass(AbstractCommand::class); - $containerReflection = $parentReflection->getProperty('container'); - $containerReflection->setAccessible(true); - $containerReflection->setValue($command, null); - - $commandReflection = new \ReflectionObject($command); - $initializer = $commandReflection->getMethod('initializeContainer'); - $initializer->setAccessible(true); - $initializer->invoke($command); - - // Replace handlers builders by mocks - $handler = new TestHandler(); - - $handlerBuilder = $this->getMockBuilder(HandlerBuilderInterface::class)->getMock(); - $handlerBuilder - ->expects($this->exactly(2)) - ->method('createHandler') - ->willReturn($handler); - - /** @var ContainerInterface $container */ - $container = $containerReflection->getValue($command); - $container->set('acmephp.monitoring.email', $handlerBuilder); - $container->set('acmephp.monitoring.slack', $handlerBuilder); - - // Introduce HTTP issue - $container->set('http.raw_client', new Client(['handler' => new MockHandler([ - new RequestException('Error Communicating with Server', new Request('GET', 'test')), - ])])); - - // Set new container - $containerReflection->setValue($command, $container); - - /* - * Renewal with issue - */ - $commandTester = new CommandTester($command); - $thrownException = null; - - try { - $commandTester->execute([ - 'command' => $command->getName(), - 'domain' => 'acmephp.com', - '--server' => 'https://localhost:14000/dir', - '--country' => 'FR', - '--province' => 'Ile de France', - '--locality' => 'Paris', - '--organization' => 'Acme PHP', - '--unit' => 'Sales', - '--email' => 'galopintitouan@gmail.com', - '--force' => true, - ]); - } catch (AcmeCoreClientException $e) { - $thrownException = $e; - } - - $this->assertNotNull($thrownException); - $this->assertNotNull($thrownException->getPrevious()); - $this->assertInstanceOf(RequestException::class, $thrownException->getPrevious()); - - $records = $handler->getRecords(); - - $this->assertCount(2, $records); - $this->assertSame(Logger::ALERT, $records[0]['level']); - $this->assertSame('A critical error occured during certificate renewal', $records[0]['message']); - $this->assertSame(Logger::ALERT, $records[1]['level']); - $this->assertSame('A critical error occured during certificate renewal', $records[1]['message']); - } -} diff --git a/tests/Cli/Repository/AbstractRepositoryTest.php b/tests/Cli/Repository/RepositoryTest.php similarity index 88% rename from tests/Cli/Repository/AbstractRepositoryTest.php rename to tests/Cli/Repository/RepositoryTest.php index 10680647..6ce933d3 100644 --- a/tests/Cli/Repository/AbstractRepositoryTest.php +++ b/tests/Cli/Repository/RepositoryTest.php @@ -21,13 +21,13 @@ use AcmePhp\Ssl\PrivateKey; use AcmePhp\Ssl\PublicKey; use League\Flysystem\Filesystem; -use League\Flysystem\Memory\MemoryAdapter; +use League\Flysystem\InMemory\InMemoryFilesystemAdapter; use PHPUnit\Framework\TestCase; use Symfony\Component\Serializer\Encoder\JsonEncoder; -use Symfony\Component\Serializer\Normalizer\GetSetMethodNormalizer; +use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; use Symfony\Component\Serializer\Serializer; -abstract class AbstractRepositoryTest extends TestCase +class RepositoryTest extends TestCase { /** * @var Serializer @@ -37,39 +37,30 @@ abstract class AbstractRepositoryTest extends TestCase /** * @var Filesystem */ - protected $master; - - /** - * @var Filesystem - */ - protected $backup; + protected $storage; /** * @var Repository */ protected $repository; - public function setUp() + public function setUp(): void { $this->serializer = new Serializer( - [new PemNormalizer(), new GetSetMethodNormalizer()], + [new PemNormalizer(), new ObjectNormalizer()], [new PemEncoder(), new JsonEncoder()] ); - $this->master = new Filesystem(new MemoryAdapter()); - $this->backup = new Filesystem(new MemoryAdapter()); - - $this->repository = $this->createRepository(); + $this->storage = new Filesystem(new InMemoryFilesystemAdapter()); + $this->repository = new Repository($this->serializer, $this->storage); } - abstract protected function createRepository(); - public function testStoreAccountKeyPair() { $this->repository->storeAccountKeyPair(new KeyPair(new PublicKey('public'), new PrivateKey('private'))); - $this->assertEquals("public\n", $this->master->read('account/key.public.pem')); - $this->assertEquals("private\n", $this->master->read('account/key.private.pem')); + $this->assertEquals("public\n", $this->storage->read('account/key.public.pem')); + $this->assertEquals("private\n", $this->storage->read('account/key.private.pem')); } public function testLoadAccountKeyPair() @@ -92,8 +83,8 @@ public function testStoreDomainKeyPair() { $this->repository->storeDomainKeyPair('example.com', new KeyPair(new PublicKey('public'), new PrivateKey('private'))); - $this->assertEquals("public\n", $this->master->read('certs/example.com/private/key.public.pem')); - $this->assertEquals("private\n", $this->master->read('certs/example.com/private/key.private.pem')); + $this->assertEquals("public\n", $this->storage->read('certs/example.com/private/key.public.pem')); + $this->assertEquals("private\n", $this->storage->read('certs/example.com/private/key.private.pem')); } public function testLoadDomainKeyPair() @@ -125,7 +116,7 @@ public function testStoreDomainAuthorizationChallenge() $this->repository->storeDomainAuthorizationChallenge('example.com', $challenge); - $json = $this->master->read('var/example.com/authorization_challenge.json'); + $json = $this->storage->read('var/example.com/authorization_challenge.json'); $this->assertJson($json); $data = json_decode($json, true); @@ -175,7 +166,7 @@ public function testStoreDomainDistinguishedName() $this->repository->storeDomainDistinguishedName('example.com', $dn); - $json = $this->master->read('var/example.com/distinguished_name.json'); + $json = $this->storage->read('var/example.com/distinguished_name.json'); $this->assertJson($json); $data = json_decode($json, true); @@ -222,10 +213,10 @@ public function testStoreDomainCertificate() $this->repository->storeDomainKeyPair('example.com', new KeyPair(new PublicKey('public'), new PrivateKey('private'))); $this->repository->storeDomainCertificate('example.com', $cert); - $this->assertEquals(self::$certPem."\n".self::$issuerCertPem."\nprivate\n", $this->master->read('certs/example.com/private/combined.pem')); - $this->assertEquals(self::$certPem."\n", $this->master->read('certs/example.com/public/cert.pem')); - $this->assertEquals(self::$issuerCertPem."\n", $this->master->read('certs/example.com/public/chain.pem')); - $this->assertEquals(self::$certPem."\n".self::$issuerCertPem."\n", $this->master->read('certs/example.com/public/fullchain.pem')); + $this->assertEquals(self::$certPem."\n".self::$issuerCertPem."\nprivate\n", $this->storage->read('certs/example.com/private/combined.pem')); + $this->assertEquals(self::$certPem."\n", $this->storage->read('certs/example.com/public/cert.pem')); + $this->assertEquals(self::$issuerCertPem."\n", $this->storage->read('certs/example.com/public/chain.pem')); + $this->assertEquals(self::$certPem."\n".self::$issuerCertPem."\n", $this->storage->read('certs/example.com/public/fullchain.pem')); } public function testLoadDomainCertificate() diff --git a/tests/Cli/Repository/RepositoryWithBackupTest.php b/tests/Cli/Repository/RepositoryWithBackupTest.php deleted file mode 100644 index ef652987..00000000 --- a/tests/Cli/Repository/RepositoryWithBackupTest.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\AcmePhp\Cli\Repository; - -use AcmePhp\Cli\Repository\Repository; - -class RepositoryWithBackupTest extends AbstractRepositoryTest -{ - protected function createRepository() - { - return new Repository($this->serializer, $this->master, $this->backup, true); - } - - public function testStoreAccountKeyPair() - { - parent::testStoreAccountKeyPair(); - - $this->assertEquals("public\n", $this->backup->read('account/key.public.pem')); - $this->assertEquals("private\n", $this->backup->read('account/key.private.pem')); - } - - public function testStoreDomainKeyPair() - { - parent::testStoreDomainKeyPair(); - - $this->assertEquals("public\n", $this->backup->read('certs/example.com/private/key.public.pem')); - $this->assertEquals("private\n", $this->backup->read('certs/example.com/private/key.private.pem')); - } - - public function testStoreDomainDistinguishedName() - { - parent::testStoreDomainDistinguishedName(); - - $this->assertJson($this->backup->read('var/example.com/distinguished_name.json')); - } - - public function testStoreDomainCertificate() - { - parent::testStoreDomainCertificate(); - - $this->assertEquals(self::$certPem."\n".self::$issuerCertPem."\nprivate\n", $this->backup->read('certs/example.com/private/combined.pem')); - $this->assertEquals(self::$certPem."\n", $this->backup->read('certs/example.com/public/cert.pem')); - $this->assertEquals(self::$issuerCertPem."\n", $this->backup->read('certs/example.com/public/chain.pem')); - $this->assertEquals(self::$certPem."\n".self::$issuerCertPem."\n", $this->backup->read('certs/example.com/public/fullchain.pem')); - } -} diff --git a/tests/Cli/Repository/RepositoryWithoutBackupTest.php b/tests/Cli/Repository/RepositoryWithoutBackupTest.php deleted file mode 100644 index cce36ca3..00000000 --- a/tests/Cli/Repository/RepositoryWithoutBackupTest.php +++ /dev/null @@ -1,55 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Tests\AcmePhp\Cli\Repository; - -use AcmePhp\Cli\Repository\Repository; - -class RepositoryWithoutBackupTest extends AbstractRepositoryTest -{ - protected function createRepository() - { - return new Repository($this->serializer, $this->master, $this->backup, false); - } - - public function testStoreAccountKeyPair() - { - parent::testStoreAccountKeyPair(); - - $this->assertFalse($this->backup->has('account/key.public.pem')); - $this->assertFalse($this->backup->has('account/key.private.pem')); - } - - public function testStoreDomainKeyPair() - { - parent::testStoreDomainKeyPair(); - - $this->assertFalse($this->backup->has('certs/acmephp.com/private/key.public.pem')); - $this->assertFalse($this->backup->has('certs/acmephp.com/private/key.private.pem')); - } - - public function testStoreDomainDistinguishedName() - { - parent::testStoreDomainDistinguishedName(); - - $this->assertFalse($this->backup->has('var/example.com/distinguished_name.json')); - } - - public function testStoreDomainCertificate() - { - parent::testStoreDomainCertificate(); - - $this->assertFalse($this->backup->has('certs/example.com/private/combined.pem')); - $this->assertFalse($this->backup->has('certs/example.com/public/cert.pem')); - $this->assertFalse($this->backup->has('certs/example.com/public/chain.pem')); - $this->assertFalse($this->backup->has('certs/example.com/public/fullchain.pem')); - } -} diff --git a/tests/Cli/SftpNginxProxyApplicationTest.php b/tests/Cli/SftpNginxProxyApplicationTest.php index 513c250a..6a6b8154 100644 --- a/tests/Cli/SftpNginxProxyApplicationTest.php +++ b/tests/Cli/SftpNginxProxyApplicationTest.php @@ -12,16 +12,12 @@ namespace Tests\AcmePhp\Cli; use League\Flysystem\Filesystem; -use League\Flysystem\Sftp\SftpAdapter; -use Tests\AcmePhp\Cli\Mock\AbstractTestApplication; -use Tests\AcmePhp\Cli\Mock\SftpNginxProxyApplication; +use League\Flysystem\PhpseclibV3\SftpAdapter; +use League\Flysystem\PhpseclibV3\SftpConnectionProvider; class SftpNginxProxyApplicationTest extends AbstractApplicationTest { - /** - * @return array - */ - protected function getFixturesDirectories() + protected function getFixturesDirectories(): array { return [ __DIR__.'/../Cli/Fixtures/local/backup', @@ -30,28 +26,27 @@ protected function getFixturesDirectories() ]; } - /** - * @return AbstractTestApplication - */ - protected function createApplication() + protected function getConfigDir(): string { - return new SftpNginxProxyApplication(); + return __DIR__.'/Fixtures/config/sfpt_nginxproxy'; } public function testFullProcess() { - $sftpFilesystem = new Filesystem(new SftpAdapter([ - 'host' => 'localhost', - 'port' => 8022, - 'username' => 'acmephp', - 'password' => 'acmephp', - 'root' => '/share', - ])); + $sftpFilesystem = new Filesystem(new SftpAdapter( + new SftpConnectionProvider( + host: 'localhost', + port: 8022, + username: 'acmephp', + password: 'acmephp', + ), + '/share', + )); // Remove any old version of the files - $sftpFilesystem->has('private') && $sftpFilesystem->deleteDir('private'); - $sftpFilesystem->has('certs') && $sftpFilesystem->deleteDir('certs'); - $sftpFilesystem->has('nginxproxy') && $sftpFilesystem->deleteDir('nginxproxy'); + $sftpFilesystem->has('private') && $sftpFilesystem->deleteDirectory('private'); + $sftpFilesystem->has('certs') && $sftpFilesystem->deleteDirectory('certs'); + $sftpFilesystem->has('nginxproxy') && $sftpFilesystem->deleteDirectory('nginxproxy'); // Run the original full process parent::testFullProcess(); @@ -60,18 +55,6 @@ public function testFullProcess() $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/nginxproxy/acmephp.com.crt'); $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/master/nginxproxy/acmephp.com.key'); - // Backup - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/certs/acmephp.com/private/key.private.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/certs/acmephp.com/private/key.public.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/certs/acmephp.com/private/combined.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/certs/acmephp.com/public/cert.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/certs/acmephp.com/public/chain.pem'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/certs/acmephp.com/public/fullchain.pem'); - - // Backup nginxproxy - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/nginxproxy/acmephp.com.crt'); - $this->assertFileExists(__DIR__.'/../Cli/Fixtures/local/backup/nginxproxy/acmephp.com.key'); - // SFTP $this->assertTrue($sftpFilesystem->has('account/key.private.pem')); $this->assertTrue($sftpFilesystem->has('account/key.public.pem')); diff --git a/tests/Cli/SimpleApplicationTest.php b/tests/Cli/SimpleApplicationTest.php index ca01697e..edc40fcb 100644 --- a/tests/Cli/SimpleApplicationTest.php +++ b/tests/Cli/SimpleApplicationTest.php @@ -11,26 +11,17 @@ namespace Tests\AcmePhp\Cli; -use Tests\AcmePhp\Cli\Mock\AbstractTestApplication; -use Tests\AcmePhp\Cli\Mock\SimpleApplication; - class SimpleApplicationTest extends AbstractApplicationTest { - /** - * @return array - */ - protected function getFixturesDirectories() + protected function getFixturesDirectories(): array { return [ __DIR__.'/../Cli/Fixtures/local/master', ]; } - /** - * @return AbstractTestApplication - */ - protected function createApplication() + protected function getConfigDir(): string { - return new SimpleApplication(); + return __DIR__.'/Fixtures/config/simple'; } } diff --git a/tests/Core/AcmeClientTest.php b/tests/Core/AcmeClientTest.php index 8a0f6bcf..302ab02b 100644 --- a/tests/Core/AcmeClientTest.php +++ b/tests/Core/AcmeClientTest.php @@ -18,6 +18,7 @@ use AcmePhp\Core\Http\SecureHttpClient; use AcmePhp\Core\Http\ServerErrorHandler; use AcmePhp\Core\Protocol\AuthorizationChallenge; +use AcmePhp\Core\Protocol\ExternalAccount; use AcmePhp\Ssl\Certificate; use AcmePhp\Ssl\CertificateRequest; use AcmePhp\Ssl\CertificateResponse; @@ -32,10 +33,19 @@ class AcmeClientTest extends AbstractFunctionnalTest { + public function provideFullProcess() + { + yield 'rsa1024' => [new RsaKeyOption(1024), false]; + yield 'rsa1024-alternate' => [new RsaKeyOption(1024), true]; + yield 'rsa4098' => [new RsaKeyOption(4098), false]; + yield 'ecprime256v1' => [new EcKeyOption('prime256v1'), false]; + yield 'ecsecp384r1' => [new EcKeyOption('secp384r1'), false]; + } + /** - * @dataProvider getKeyOptions + * @dataProvider provideFullProcess */ - public function testFullProcess(KeyOption $keyOption) + public function testFullProcess(KeyOption $keyOption, bool $useAlternateCertificate) { $secureHttpClient = new SecureHttpClient( (new KeyPairGenerator())->generateKeyPair($keyOption), @@ -51,7 +61,11 @@ public function testFullProcess(KeyOption $keyOption) /* * Register account */ - $data = $client->registerAccount(); + if ('eab' === getenv('PEBBLE_MODE')) { + $data = $client->registerAccount('titouan.galopin@acmephp.com', new ExternalAccount('kid1', 'dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5n')); + } else { + $data = $client->registerAccount('titouan.galopin@acmephp.com'); + } $this->assertIsArray($data); $this->assertArrayHasKey('key', $data); @@ -62,6 +76,7 @@ public function testFullProcess(KeyOption $keyOption) * Ask for domain challenge */ $order = $client->requestOrder(['acmephp.com']); + $this->assertEquals('pending', $order->getStatus()); $challenges = $order->getAuthorizationChallenges('acmephp.com'); foreach ($challenges as $challenge) { if ('http-01' === $challenge->getType()) { @@ -86,11 +101,20 @@ public function testFullProcess(KeyOption $keyOption) $this->cleanChallenge($challenge->getToken()); } + /** + * Reload order, check if challenge was completed. + */ + $updatedOrder = $client->reloadOrder($order); + $this->assertEquals('ready', $updatedOrder->getStatus()); + $this->assertCount(1, $updatedOrder->getAuthorizationChallenges('acmephp.com')); + $validatedChallenge = $updatedOrder->getAuthorizationChallenges('acmephp.com')[0]; + $this->assertEquals('valid', $validatedChallenge->getStatus()); + /* * Request certificate */ $csr = new CertificateRequest(new DistinguishedName('acmephp.com'), (new KeyPairGenerator())->generateKeyPair($keyOption)); - $response = $client->finalizeOrder($order, $csr); + $response = $client->finalizeOrder($order, $csr, 180, $useAlternateCertificate); $this->assertInstanceOf(CertificateResponse::class, $response); $this->assertEquals($csr, $response->getCertificateRequest()); @@ -108,13 +132,36 @@ public function testFullProcess(KeyOption $keyOption) } } - public function getKeyOptions() + /** + * @dataProvider provideFullProcess + */ + public function testRequestAuthorizationAllowsCapitalisation(KeyOption $keyOption, bool $useAlternateCertificate) { - yield [new RsaKeyOption(1024)]; - yield [new RsaKeyOption(4098)]; - if (\PHP_VERSION_ID >= 70100) { - yield [new EcKeyOption('prime256v1')]; - yield [new EcKeyOption('secp384r1')]; + $secureHttpClient = new SecureHttpClient( + (new KeyPairGenerator())->generateKeyPair($keyOption), + new Client(), + new Base64SafeEncoder(), + new KeyParser(), + new DataSigner(), + new ServerErrorHandler() + ); + + $client = new AcmeClient($secureHttpClient, 'https://localhost:14000/dir'); + + /* + * Register account + */ + if ('eab' === getenv('PEBBLE_MODE')) { + $client->registerAccount('titouan.galopin@acmephp.com', new ExternalAccount('kid1', 'dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5n')); + } else { + $client->registerAccount('titouan.galopin@acmephp.com'); } + + /* + * Request authorization challenges using a domain with capital letters + */ + $challenges = $client->requestAuthorization('ACMEPHP.com'); + + $this->assertNotEmpty($challenges); } } diff --git a/tests/Core/Challenge/ChainValidatorTest.php b/tests/Core/Challenge/ChainValidatorTest.php index bf0d8e17..f78e6650 100644 --- a/tests/Core/Challenge/ChainValidatorTest.php +++ b/tests/Core/Challenge/ChainValidatorTest.php @@ -12,27 +12,32 @@ namespace Tests\AcmePhp\Core\Challenge; use AcmePhp\Core\Challenge\ChainValidator; +use AcmePhp\Core\Challenge\SolverInterface; use AcmePhp\Core\Challenge\ValidatorInterface; use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; class ChainValidatorTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $mockValidator1 = $this->prophesize(ValidatorInterface::class); $mockValidator2 = $this->prophesize(ValidatorInterface::class); $dummyChallenge = $this->prophesize(AuthorizationChallenge::class)->reveal(); + $solver = $this->prophesize(SolverInterface::class)->reveal(); $validator = new ChainValidator([$mockValidator1->reveal(), $mockValidator2->reveal()]); - $mockValidator1->supports($dummyChallenge)->willReturn(false); - $mockValidator2->supports($dummyChallenge)->willReturn(true); - $this->assertTrue($validator->supports($dummyChallenge)); + $mockValidator1->supports($dummyChallenge, $solver)->willReturn(false); + $mockValidator2->supports($dummyChallenge, $solver)->willReturn(true); + $this->assertTrue($validator->supports($dummyChallenge, $solver)); - $mockValidator1->supports($dummyChallenge)->willReturn(false); - $mockValidator2->supports($dummyChallenge)->willReturn(false); - $this->assertFalse($validator->supports($dummyChallenge)); + $mockValidator1->supports($dummyChallenge, $solver)->willReturn(false); + $mockValidator2->supports($dummyChallenge, $solver)->willReturn(false); + $this->assertFalse($validator->supports($dummyChallenge, $solver)); } public function testIsValid() @@ -40,14 +45,15 @@ public function testIsValid() $mockValidator1 = $this->prophesize(ValidatorInterface::class); $mockValidator2 = $this->prophesize(ValidatorInterface::class); $dummyChallenge = $this->prophesize(AuthorizationChallenge::class)->reveal(); + $solver = $this->prophesize(SolverInterface::class)->reveal(); $validator = new ChainValidator([$mockValidator1->reveal(), $mockValidator2->reveal()]); - $mockValidator1->supports($dummyChallenge)->willReturn(false); - $mockValidator1->isValid($dummyChallenge)->shouldNotBeCalled(); - $mockValidator2->supports($dummyChallenge)->willReturn(true); - $mockValidator2->isValid($dummyChallenge)->willReturn(true); + $mockValidator1->supports($dummyChallenge, $solver)->willReturn(false); + $mockValidator1->isValid($dummyChallenge, $solver)->shouldNotBeCalled(); + $mockValidator2->supports($dummyChallenge, $solver)->willReturn(true); + $mockValidator2->isValid($dummyChallenge, $solver)->willReturn(true); - $this->assertTrue($validator->isValid($dummyChallenge)); + $this->assertTrue($validator->isValid($dummyChallenge, $solver)); } } diff --git a/tests/Core/Challenge/Dns/DnsDataExtractorTest.php b/tests/Core/Challenge/Dns/DnsDataExtractorTest.php index 9d232fe5..2ed8ab13 100644 --- a/tests/Core/Challenge/Dns/DnsDataExtractorTest.php +++ b/tests/Core/Challenge/Dns/DnsDataExtractorTest.php @@ -15,9 +15,12 @@ use AcmePhp\Core\Http\Base64SafeEncoder; use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; class DnsDataExtractorTest extends TestCase { + use ProphecyTrait; + public function testGetRecordName() { $domain = 'foo.com'; diff --git a/tests/Core/Challenge/Dns/DnsValidatorTest.php b/tests/Core/Challenge/Dns/DnsValidatorTest.php index cc3fe403..5102380b 100644 --- a/tests/Core/Challenge/Dns/DnsValidatorTest.php +++ b/tests/Core/Challenge/Dns/DnsValidatorTest.php @@ -14,11 +14,15 @@ use AcmePhp\Core\Challenge\Dns\DnsDataExtractor; use AcmePhp\Core\Challenge\Dns\DnsResolverInterface; use AcmePhp\Core\Challenge\Dns\DnsValidator; +use AcmePhp\Core\Challenge\SolverInterface; use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; class DnsValidatorTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; @@ -30,10 +34,10 @@ public function testSupports() $validator = new DnsValidator($mockExtractor->reveal()); $stubChallenge->getType()->willReturn($typeDns); - $this->assertTrue($validator->supports($stubChallenge->reveal())); + $this->assertTrue($validator->supports($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); $stubChallenge->getType()->willReturn($typeHttp); - $this->assertFalse($validator->supports($stubChallenge->reveal())); + $this->assertFalse($validator->supports($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); } public function testIsValid() @@ -51,7 +55,7 @@ public function testIsValid() $mockExtractor->getRecordName($stubChallenge->reveal())->willReturn($recordName); $mockExtractor->getRecordValue($stubChallenge->reveal())->willReturn($recordValue); - $this->assertTrue($validator->isValid($stubChallenge->reveal())); + $this->assertTrue($validator->isValid($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); } public function testIsValidCheckRecordValue() @@ -69,6 +73,6 @@ public function testIsValidCheckRecordValue() $mockExtractor->getRecordName($stubChallenge->reveal())->willReturn($recordName); $mockExtractor->getRecordValue($stubChallenge->reveal())->willReturn($recordValue); - $this->assertFalse($validator->isValid($stubChallenge->reveal())); + $this->assertFalse($validator->isValid($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); } } diff --git a/tests/Core/Challenge/Dns/GandiSolverTest.php b/tests/Core/Challenge/Dns/GandiSolverTest.php index 19915113..0ed9adc6 100644 --- a/tests/Core/Challenge/Dns/GandiSolverTest.php +++ b/tests/Core/Challenge/Dns/GandiSolverTest.php @@ -16,9 +16,12 @@ use AcmePhp\Core\Protocol\AuthorizationChallenge; use GuzzleHttp\ClientInterface; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; class GandiSolverTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; diff --git a/tests/Core/Challenge/Dns/Route53SolverTest.php b/tests/Core/Challenge/Dns/Route53SolverTest.php index bd9581af..cdbaca5c 100644 --- a/tests/Core/Challenge/Dns/Route53SolverTest.php +++ b/tests/Core/Challenge/Dns/Route53SolverTest.php @@ -17,9 +17,12 @@ use Aws\Route53\Route53Client; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; class Route53SolverTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; diff --git a/tests/Core/Challenge/Dns/SimpleDnsSolverTest.php b/tests/Core/Challenge/Dns/SimpleDnsSolverTest.php index 7ef38e5d..09be9d71 100644 --- a/tests/Core/Challenge/Dns/SimpleDnsSolverTest.php +++ b/tests/Core/Challenge/Dns/SimpleDnsSolverTest.php @@ -16,10 +16,13 @@ use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Console\Output\OutputInterface; class SimpleDnsSolverTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; diff --git a/tests/Core/Challenge/Http/FilesystemSolverTest.php b/tests/Core/Challenge/Http/FilesystemSolverTest.php index 3c46c23c..75e8e4d1 100644 --- a/tests/Core/Challenge/Http/FilesystemSolverTest.php +++ b/tests/Core/Challenge/Http/FilesystemSolverTest.php @@ -14,14 +14,17 @@ use AcmePhp\Core\Challenge\Http\FilesystemSolver; use AcmePhp\Core\Challenge\Http\HttpDataExtractor; use AcmePhp\Core\Filesystem\FilesystemFactoryInterface; +use AcmePhp\Core\Filesystem\FilesystemInterface; use AcmePhp\Core\Protocol\AuthorizationChallenge; -use League\Flysystem\FilesystemInterface; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Container\ContainerInterface; class FilesystemSolverTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; @@ -43,21 +46,23 @@ public function testSolve() $checkPath = '/.challenge'; $checkContent = 'randomPayload'; - $mockExtractor = $this->prophesize(HttpDataExtractor::class); - $mockLocator = $this->prophesize(ContainerInterface::class); - $mockFlysystemFactory = $this->prophesize(FilesystemFactoryInterface::class); - $mockFlysystem = $this->prophesize(FilesystemInterface::class); $stubChallenge = $this->prophesize(AuthorizationChallenge::class); - $solver = new FilesystemSolver($mockLocator->reveal(), $mockExtractor->reveal()); - - $mockLocator->get('stub')->willReturn($mockFlysystemFactory->reveal()); - $mockFlysystemFactory->create(Argument::any())->willReturn($mockFlysystem->reveal()); + $mockExtractor = $this->prophesize(HttpDataExtractor::class); $mockExtractor->getCheckPath($stubChallenge->reveal())->willReturn($checkPath); $mockExtractor->getCheckContent($stubChallenge->reveal())->willReturn($checkContent); + $mockFlysystem = $this->prophesize(FilesystemInterface::class); $mockFlysystem->write($checkPath, $checkContent)->shouldBeCalled(); + $mockFlysystemFactory = $this->prophesize(FilesystemFactoryInterface::class); + $mockFlysystemFactory->create(Argument::any())->willReturn($mockFlysystem->reveal()); + + $mockLocator = $this->prophesize(ContainerInterface::class); + $mockLocator->get('stub')->willReturn($mockFlysystemFactory->reveal()); + + $solver = new FilesystemSolver($mockLocator->reveal(), $mockExtractor->reveal()); + $solver->configure(['adapter' => 'stub']); $solver->solve($stubChallenge->reveal()); } @@ -66,17 +71,20 @@ public function testCleanup() { $checkPath = '/.challenge'; + $stubChallenge = $this->prophesize(AuthorizationChallenge::class); + $mockExtractor = $this->prophesize(HttpDataExtractor::class); - $mockLocator = $this->prophesize(ContainerInterface::class); - $mockFlysystemFactory = $this->prophesize(FilesystemFactoryInterface::class); + $mockExtractor->getCheckPath($stubChallenge->reveal())->willReturn($checkPath); + $mockFlysystem = $this->prophesize(FilesystemInterface::class); - $stubChallenge = $this->prophesize(AuthorizationChallenge::class); - $solver = new FilesystemSolver($mockLocator->reveal(), $mockExtractor->reveal()); + $mockFlysystemFactory = $this->prophesize(FilesystemFactoryInterface::class); + $mockFlysystemFactory->create(Argument::any())->willReturn($mockFlysystem->reveal()); + $mockLocator = $this->prophesize(ContainerInterface::class); $mockLocator->get('stub')->willReturn($mockFlysystemFactory->reveal()); - $mockFlysystemFactory->create(Argument::any())->willReturn($mockFlysystem->reveal()); - $mockExtractor->getCheckPath($stubChallenge->reveal())->willReturn($checkPath); + + $solver = new FilesystemSolver($mockLocator->reveal(), $mockExtractor->reveal()); $mockFlysystem->delete($checkPath)->shouldBeCalled(); diff --git a/tests/Core/Challenge/Http/HttpDataExtractorTest.php b/tests/Core/Challenge/Http/HttpDataExtractorTest.php index f8a4b75e..5b5a8a3c 100644 --- a/tests/Core/Challenge/Http/HttpDataExtractorTest.php +++ b/tests/Core/Challenge/Http/HttpDataExtractorTest.php @@ -14,9 +14,12 @@ use AcmePhp\Core\Challenge\Http\HttpDataExtractor; use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; class HttpDataExtractorTest extends TestCase { + use ProphecyTrait; + public function testGetCheckUrl() { $domain = 'foo.com'; diff --git a/tests/Core/Challenge/Http/HttpValidatorTest.php b/tests/Core/Challenge/Http/HttpValidatorTest.php index 6cc231f4..1dd1c7e0 100644 --- a/tests/Core/Challenge/Http/HttpValidatorTest.php +++ b/tests/Core/Challenge/Http/HttpValidatorTest.php @@ -13,16 +13,20 @@ use AcmePhp\Core\Challenge\Http\HttpDataExtractor; use AcmePhp\Core\Challenge\Http\HttpValidator; +use AcmePhp\Core\Challenge\SolverInterface; use AcmePhp\Core\Protocol\AuthorizationChallenge; use GuzzleHttp\Client; use GuzzleHttp\Exception\ClientException; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\StreamInterface; class HttpValidatorTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; @@ -35,10 +39,10 @@ public function testSupports() $validator = new HttpValidator($mockExtractor->reveal(), $mockHttpClient->reveal()); $stubChallenge->getType()->willReturn($typeDns); - $this->assertFalse($validator->supports($stubChallenge->reveal())); + $this->assertFalse($validator->supports($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); $stubChallenge->getType()->willReturn($typeHttp); - $this->assertTrue($validator->supports($stubChallenge->reveal())); + $this->assertTrue($validator->supports($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); } public function testIsValid() @@ -57,11 +61,11 @@ public function testIsValid() $mockExtractor->getCheckUrl($stubChallenge->reveal())->willReturn($checkUrl); $mockExtractor->getCheckContent($stubChallenge->reveal())->willReturn($checkContent); - $mockHttpClient->get($checkUrl)->willReturn($stubResponse->reveal()); + $mockHttpClient->get($checkUrl, ['verify' => false])->willReturn($stubResponse->reveal()); $stubResponse->getBody()->willReturn($stubStream->reveal()); $stubStream->getContents()->willReturn($checkContent); - $this->assertTrue($validator->isValid($stubChallenge->reveal())); + $this->assertTrue($validator->isValid($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); } public function testIsValidCatchExceptions() @@ -78,12 +82,15 @@ public function testIsValidCatchExceptions() $mockExtractor->getCheckUrl($stubChallenge->reveal())->willReturn($checkUrl); $mockExtractor->getCheckContent($stubChallenge->reveal())->willReturn($checkContent); - $mockHttpClient->get($checkUrl)->willThrow(new ClientException( + $mockResponse = $this->prophesize(ResponseInterface::class); + $mockResponse->getStatusCode()->willReturn(400); + + $mockHttpClient->get($checkUrl, ['verify' => false])->willThrow(new ClientException( 'boom', $this->prophesize(RequestInterface::class)->reveal(), - $this->prophesize(ResponseInterface::class)->reveal() + $mockResponse->reveal() )); - $this->assertFalse($validator->isValid($stubChallenge->reveal())); + $this->assertFalse($validator->isValid($stubChallenge->reveal(), $this->prophesize(SolverInterface::class)->reveal())); } } diff --git a/tests/Core/Challenge/Http/SimpleHttpSolverTest.php b/tests/Core/Challenge/Http/SimpleHttpSolverTest.php index 3c6df104..7b4feb6f 100644 --- a/tests/Core/Challenge/Http/SimpleHttpSolverTest.php +++ b/tests/Core/Challenge/Http/SimpleHttpSolverTest.php @@ -16,10 +16,13 @@ use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; use Prophecy\Argument; +use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Component\Console\Output\OutputInterface; class SimpleHttpSolverTest extends TestCase { + use ProphecyTrait; + public function testSupports() { $typeDns = 'dns-01'; diff --git a/tests/Core/Challenge/WaitingValidatorTest.php b/tests/Core/Challenge/WaitingValidatorTest.php index a2f19d67..c16ea383 100644 --- a/tests/Core/Challenge/WaitingValidatorTest.php +++ b/tests/Core/Challenge/WaitingValidatorTest.php @@ -11,15 +11,19 @@ namespace Tests\AcmePhp\Core\Challenge; +use AcmePhp\Core\Challenge\SolverInterface; use AcmePhp\Core\Challenge\ValidatorInterface; use AcmePhp\Core\Challenge\WaitingValidator; use AcmePhp\Core\Protocol\AuthorizationChallenge; use PHPUnit\Framework\TestCase; +use Prophecy\PhpUnit\ProphecyTrait; use Symfony\Bridge\PhpUnit\ClockMock; class WaitingValidatorTest extends TestCase { - public function setUp() + use ProphecyTrait; + + public function setUp(): void { parent::setUp(); @@ -28,7 +32,7 @@ public function setUp() ClockMock::withClockMock(true); } - public function tearDown() + public function tearDown(): void { parent::tearDown(); @@ -39,26 +43,28 @@ public function testSupports() { $mockDecorated = $this->prophesize(ValidatorInterface::class); $dummyChallenge = $this->prophesize(AuthorizationChallenge::class)->reveal(); + $solver = $this->prophesize(SolverInterface::class)->reveal(); $validator = new WaitingValidator($mockDecorated->reveal()); - $mockDecorated->supports($dummyChallenge)->willReturn(true); - $this->assertTrue($validator->supports($dummyChallenge)); + $mockDecorated->supports($dummyChallenge, $solver)->willReturn(true); + $this->assertTrue($validator->supports($dummyChallenge, $solver)); - $mockDecorated->supports($dummyChallenge)->willReturn(false); - $this->assertFalse($validator->supports($dummyChallenge)); + $mockDecorated->supports($dummyChallenge, $solver)->willReturn(false); + $this->assertFalse($validator->supports($dummyChallenge, $solver)); } public function testIsValid() { $mockDecorated = $this->prophesize(ValidatorInterface::class); $dummyChallenge = $this->prophesize(AuthorizationChallenge::class)->reveal(); + $solver = $this->prophesize(SolverInterface::class)->reveal(); $validator = new WaitingValidator($mockDecorated->reveal()); $start = time(); - $mockDecorated->isValid($dummyChallenge)->willReturn(true); - $this->assertTrue($validator->isValid($dummyChallenge)); + $mockDecorated->isValid($dummyChallenge, $solver)->willReturn(true); + $this->assertTrue($validator->isValid($dummyChallenge, $solver)); $this->assertLessThan(1, time() - $start); } @@ -66,12 +72,13 @@ public function testIsValidWaitBetweenTests() { $mockDecorated = $this->prophesize(ValidatorInterface::class); $dummyChallenge = $this->prophesize(AuthorizationChallenge::class)->reveal(); + $solver = $this->prophesize(SolverInterface::class)->reveal(); $validator = new WaitingValidator($mockDecorated->reveal()); $start = time(); - $mockDecorated->isValid($dummyChallenge)->willReturn(false); - $this->assertFalse($validator->isValid($dummyChallenge)); + $mockDecorated->isValid($dummyChallenge, $solver)->willReturn(false); + $this->assertFalse($validator->isValid($dummyChallenge, $solver)); $this->assertGreaterThanOrEqual(180, time() - $start); } @@ -79,12 +86,13 @@ public function testIsValidRetryTillOk() { $mockDecorated = $this->prophesize(ValidatorInterface::class); $dummyChallenge = $this->prophesize(AuthorizationChallenge::class)->reveal(); + $solver = $this->prophesize(SolverInterface::class)->reveal(); $validator = new WaitingValidator($mockDecorated->reveal()); $start = time(); - $mockDecorated->isValid($dummyChallenge)->willReturn(false, false, true); - $this->assertTrue($validator->isValid($dummyChallenge)); + $mockDecorated->isValid($dummyChallenge, $solver)->willReturn(false, false, true); + $this->assertTrue($validator->isValid($dummyChallenge, $solver)); $this->assertGreaterThanOrEqual(6, time() - $start); $this->assertLessThan(9, time() - $start); } diff --git a/tests/Core/Http/SecureHttpClientTest.php b/tests/Core/Http/SecureHttpClientTest.php index 7d20abf2..c860485f 100644 --- a/tests/Core/Http/SecureHttpClientTest.php +++ b/tests/Core/Http/SecureHttpClientTest.php @@ -15,6 +15,7 @@ use AcmePhp\Core\Http\Base64SafeEncoder; use AcmePhp\Core\Http\SecureHttpClient; use AcmePhp\Core\Http\ServerErrorHandler; +use AcmePhp\Core\Protocol\ExternalAccount; use AcmePhp\Ssl\Generator\KeyPairGenerator; use AcmePhp\Ssl\Parser\KeyParser; use AcmePhp\Ssl\Signer\DataSigner; @@ -142,118 +143,28 @@ public function testInvalidJsonRequest() $client->request('GET', '/foo', ['foo' => 'bar'], true); } - /** - * @group legacy - */ - public function testRequestPayload() + public function testCreateExternalAccountPayload(): void { - $container = []; - - $stack = HandlerStack::create(new MockHandler([new Response(200, [], json_encode(['test' => 'ok']))])); - $stack->push(Middleware::history($container)); - - $keyPairGenerator = new KeyPairGenerator(); - - $dataSigner = $this->getMockBuilder(DataSigner::class)->getMock(); - $dataSigner->expects($this->once()) - ->method('signData') - ->willReturn('foobar'); - $client = new SecureHttpClient( - $keyPairGenerator->generateKeyPair(), - new Client(['handler' => $stack]), + (new KeyPairGenerator())->generateKeyPair(), + new Client(), new Base64SafeEncoder(), new KeyParser(), - $dataSigner, - $this->getMockBuilder(ServerErrorHandler::class)->getMock() + new DataSigner(), + new ServerErrorHandler(), ); - $client->request('POST', '/acme/new-reg', $client->signJwkPayload('/acme/new-reg', ['contact' => 'foo@bar.com']), true); - - // Check request object - $this->assertCount(1, $container); - - /** @var RequestInterface $request */ - $request = $container[0]['request']; - - $this->assertInstanceOf(RequestInterface::class, $request); - $this->assertEquals('POST', $request->getMethod()); - $this->assertEquals('/acme/new-reg', ($request->getUri() instanceof Uri) ? $request->getUri()->getPath() : $request->getUri()); - - $body = \GuzzleHttp\Psr7\copy_to_string($request->getBody()); - $payload = @json_decode($body, true); + $payload = $client->createExternalAccountPayload(new ExternalAccount('id', str_repeat('hmacKey', '100')), 'bar'); - $this->assertIsArray($payload); $this->assertArrayHasKey('protected', $payload); + $this->assertSame('eyJhbGciOiJIUzI1NiIsImtpZCI6ImlkIiwidXJsIjoiYmFyIn0', $payload['protected']); $this->assertArrayHasKey('payload', $payload); + $this->assertStringStartsWith('ey', $payload['payload']); $this->assertArrayHasKey('signature', $payload); - $this->assertEquals('Zm9vYmFy', $payload['signature']); - } - - /** - * @group legacy - */ - public function testValidUnsignedStringRequest() - { - $client = $this->createMockedClient([new Response(200, [], 'foo')], false); - $body = $client->unsignedRequest('GET', '/foo', ['foo' => 'bar'], false); - $this->assertEquals('foo', $body); - } - - /** - * @group legacy - */ - public function testValidUnsignedJsonRequest() - { - $client = $this->createMockedClient([new Response(200, [], json_encode(['test' => 'ok']))], false); - $data = $client->unsignedRequest('GET', '/foo', ['foo' => 'bar'], true); - $this->assertEquals(['test' => 'ok'], $data); - } - - /** - * @group legacy - */ - public function testInvalidUnsignedJsonRequest() - { - $this->expectException('AcmePhp\Core\Exception\Protocol\ExpectedJsonException'); - $client = $this->createMockedClient([new Response(200, [], 'invalid json')], false); - $client->unsignedRequest('GET', '/foo', ['foo' => 'bar'], true); - } - - /** - * @group legacy - */ - public function testValidSignedStringRequest() - { - $client = $this->createMockedClient([new Response(200, [], 'foo')], false); - $body = $client->signedRequest('GET', '/foo', ['foo' => 'bar'], false); - $this->assertEquals('foo', $body); - } - - /** - * @group legacy - */ - public function testValidSignedJsonRequest() - { - $client = $this->createMockedClient([new Response(200, [], json_encode(['test' => 'ok']))], false); - $data = $client->signedRequest('GET', '/foo', ['foo' => 'bar'], true); - $this->assertEquals(['test' => 'ok'], $data); - } - - /** - * @group legacy - */ - public function testInvalidSignedJsonRequest() - { - $this->expectException('AcmePhp\Core\Exception\Protocol\ExpectedJsonException'); - $client = $this->createMockedClient([new Response(200, [], 'invalid json')], false); - $client->signedRequest('GET', '/foo', ['foo' => 'bar'], true); + $this->assertNotEmpty($payload['signature']); } - /** - * @group legacy - */ - public function testSignedRequestPayload() + public function testRequestPayload() { $container = []; @@ -276,7 +187,7 @@ public function testSignedRequestPayload() $this->getMockBuilder(ServerErrorHandler::class)->getMock() ); - $client->signedRequest('POST', '/acme/new-reg', ['contact' => 'foo@bar.com'], true); + $client->request('POST', '/acme/new-reg', $client->signJwkPayload('/acme/new-reg', ['contact' => 'foo@bar.com']), true); // Check request object $this->assertCount(1, $container); @@ -288,8 +199,7 @@ public function testSignedRequestPayload() $this->assertEquals('POST', $request->getMethod()); $this->assertEquals('/acme/new-reg', ($request->getUri() instanceof Uri) ? $request->getUri()->getPath() : $request->getUri()); - $body = \GuzzleHttp\Psr7\copy_to_string($request->getBody()); - $payload = @json_decode($body, true); + $payload = json_decode($request->getBody()->getContents(), true, JSON_THROW_ON_ERROR); $this->assertIsArray($payload); $this->assertArrayHasKey('protected', $payload); diff --git a/tests/Core/Http/ServerErrorHandlerTest.php b/tests/Core/Http/ServerErrorHandlerTest.php index 9aacf987..49fbc835 100644 --- a/tests/Core/Http/ServerErrorHandlerTest.php +++ b/tests/Core/Http/ServerErrorHandlerTest.php @@ -14,14 +14,23 @@ use AcmePhp\Core\Exception\AcmeCoreServerException; use AcmePhp\Core\Exception\Server\BadCsrServerException; use AcmePhp\Core\Exception\Server\BadNonceServerException; +use AcmePhp\Core\Exception\Server\CaaServerException; use AcmePhp\Core\Exception\Server\ConnectionServerException; +use AcmePhp\Core\Exception\Server\DnsServerException; +use AcmePhp\Core\Exception\Server\IncorrectResponseServerException; use AcmePhp\Core\Exception\Server\InternalServerException; +use AcmePhp\Core\Exception\Server\InvalidContactServerException; use AcmePhp\Core\Exception\Server\InvalidEmailServerException; use AcmePhp\Core\Exception\Server\MalformedServerException; +use AcmePhp\Core\Exception\Server\OrderNotReadyServerException; use AcmePhp\Core\Exception\Server\RateLimitedServerException; +use AcmePhp\Core\Exception\Server\RejectedIdentifierServerException; use AcmePhp\Core\Exception\Server\TlsServerException; use AcmePhp\Core\Exception\Server\UnauthorizedServerException; use AcmePhp\Core\Exception\Server\UnknownHostServerException; +use AcmePhp\Core\Exception\Server\UnsupportedContactServerException; +use AcmePhp\Core\Exception\Server\UnsupportedIdentifierServerException; +use AcmePhp\Core\Exception\Server\UserActionRequiredServerException; use AcmePhp\Core\Http\ServerErrorHandler; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; @@ -34,14 +43,23 @@ public function getErrorTypes() return [ ['badCSR', BadCsrServerException::class], ['badNonce', BadNonceServerException::class], + ['caa', CaaServerException::class], ['connection', ConnectionServerException::class], - ['serverInternal', InternalServerException::class], + ['dns', DnsServerException::class], + ['incorrectResponse', IncorrectResponseServerException::class], + ['invalidContact', InvalidContactServerException::class], ['invalidEmail', InvalidEmailServerException::class], ['malformed', MalformedServerException::class], + ['orderNotReady', OrderNotReadyServerException::class], ['rateLimited', RateLimitedServerException::class], + ['rejectedIdentifier', RejectedIdentifierServerException::class], + ['serverInternal', InternalServerException::class], ['tls', TlsServerException::class], ['unauthorized', UnauthorizedServerException::class], ['unknownHost', UnknownHostServerException::class], + ['unsupportedContact', UnsupportedContactServerException::class], + ['unsupportedIdentifier', UnsupportedIdentifierServerException::class], + ['userActionRequired', UserActionRequiredServerException::class], ]; } diff --git a/tests/Fixtures/pebble-config-default.json b/tests/Fixtures/pebble-config-default.json new file mode 100644 index 00000000..1127939e --- /dev/null +++ b/tests/Fixtures/pebble-config-default.json @@ -0,0 +1,12 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:14000", + "managementListenAddress": "0.0.0.0:15000", + "certificate": "test/certs/localhost/cert.pem", + "privateKey": "test/certs/localhost/key.pem", + "httpPort": 5002, + "tlsPort": 5001, + "ocspResponderURL": "", + "externalAccountBindingRequired": false + } +} diff --git a/tests/Fixtures/pebble-config-eab.json b/tests/Fixtures/pebble-config-eab.json new file mode 100644 index 00000000..95a4e69b --- /dev/null +++ b/tests/Fixtures/pebble-config-eab.json @@ -0,0 +1,15 @@ +{ + "pebble": { + "listenAddress": "0.0.0.0:14000", + "managementListenAddress": "0.0.0.0:15000", + "certificate": "test/certs/localhost/cert.pem", + "privateKey": "test/certs/localhost/key.pem", + "httpPort": 5002, + "tlsPort": 5001, + "ocspResponderURL": "", + "externalAccountBindingRequired": true, + "externalAccountMACKeys": { + "kid1": "dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5n" + } + } +} diff --git a/tests/Ssl/AssertsOpenSslResource.php b/tests/Ssl/AssertsOpenSslResource.php new file mode 100644 index 00000000..47e19307 --- /dev/null +++ b/tests/Ssl/AssertsOpenSslResource.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Tests\AcmePhp\Ssl; + +use PHPUnit\Framework\Assert; + +trait AssertsOpenSslResource +{ + /** + * Asserts that the provided "resource" is an OpenSSL Assymetric Key. + * + * On PHP 8+, OpenSSL works with objects. On PHP <8 OpenSSL works with resources. + * + * @param resource|\OpenSSLAsymmetricKey $resource + */ + public function assertIsOpenSslAsymmetricKey($resource) + { + if (PHP_MAJOR_VERSION >= 8) { + Assert::assertInstanceOf(\OpenSSLAsymmetricKey::class, $resource); + } else { + Assert::assertIsResource($resource); + } + } +} diff --git a/tests/Ssl/CertificateTest.php b/tests/Ssl/CertificateTest.php index 9531a7c4..db35a756 100644 --- a/tests/Ssl/CertificateTest.php +++ b/tests/Ssl/CertificateTest.php @@ -17,10 +17,12 @@ class CertificateTest extends TestCase { - public function test getPublicKey returns a PublicKey() + use AssertsOpenSslResource; + + public function testGetPublicKeyReturnsAPublicKey() { $certificate = new Certificate( - ' + ' -----BEGIN CERTIFICATE----- MIIFkTCCBHmgAwIBAgITAP/g3ErooCmPSlx2kAVx9abKkTANBgkqhkiG9w0BAQsF ADAfMR0wGwYDVQQDExRoYXBweSBoYWNrZXIgZmFrZSBDQTAeFw0xNjAzMjUyMjI3 @@ -61,7 +63,7 @@ public function test getPublicKey returns a PublicKey() $this->assertEquals('58b94e38ce0088f0ec5a0c38f04bd76c', md5($publicKey->getPEM())); } - public function test getPublicKeyResource returns a resource() + public function testGetPublicKeyResourceReturnsAResource() { $certificate = new Certificate( ' @@ -102,6 +104,6 @@ public function test getPublicKeyResource returns a resource() $resource = $certificate->getPublicKeyResource(); - $this->assertIsResource($resource); + $this->assertIsOpenSslAsymmetricKey($resource); } } diff --git a/tests/Ssl/Generator/KeyPairGeneratorTest.php b/tests/Ssl/Generator/KeyPairGeneratorTest.php index 23a8b722..49df2602 100644 --- a/tests/Ssl/Generator/KeyPairGeneratorTest.php +++ b/tests/Ssl/Generator/KeyPairGeneratorTest.php @@ -18,44 +18,38 @@ use AcmePhp\Ssl\Generator\RsaKey\RsaKeyOption; use AcmePhp\Ssl\KeyPair; use PHPUnit\Framework\TestCase; +use Tests\AcmePhp\Ssl\AssertsOpenSslResource; class KeyPairGeneratorTest extends TestCase { + use AssertsOpenSslResource; + /** @var KeyPairGenerator */ private $service; - public function setUp() + public function setUp(): void { parent::setUp(); $this->service = new KeyPairGenerator(); } - /** - * @group legacy - */ - public function test generateKeyPair supports keysize() - { - $result = $this->service->generateKeyPair(1024); - $this->assertInstanceOf(KeyPair::class, $result); - } - - public function test generateKeyPair generate random instance of KeyPair() + public function testGenerateKeyPairGenerateRandomInstanceOfKeyPair() { $result = $this->service->generateKeyPair(new RsaKeyOption(1024)); $this->assertInstanceOf(KeyPair::class, $result); - $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $result->getPublicKey()->getPEM()); - $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $result->getPrivateKey()->getPEM()); - $this->assertIsResource($result->getPublicKey()->getResource()); - $this->assertIsResource($result->getPrivateKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPublicKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPrivateKey()->getResource()); $details = openssl_pkey_get_details($result->getPrivateKey()->getResource()); $this->assertEquals(1024, $details['bits']); $this->assertArrayHasKey('rsa', $details); + + $this->assertEquals($details['key'], $result->getPublicKey()->getPEM()); } - public function test generateKeyPair generate random instance of KeyPair using DH() + public function testGenerateKeyPairGenerateRandomInstanceOfKeyPairUsingDH() { $result = $this->service->generateKeyPair(new DhKeyOption( 'dcf93a0b883972ec0e19989ac5a2ce310e1d37717e8d9571bb7623731866e61ef75a2e27898b057f9891c2e27a639c3f29b60814581cd3b2ca3986d2683705577d45c2e7e52dc81c7a171876e5cea74b1448bfdfaf18828efd2519f14e45e3826634af1949e5b535cc829a483b8a76223e5d490a257f05bdff16f2fb22c583ab', @@ -63,46 +57,46 @@ public function test generateKeyPair generate random instance of KeyPair  )); $this->assertInstanceOf(KeyPair::class, $result); - $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $result->getPublicKey()->getPEM()); - $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $result->getPrivateKey()->getPEM()); - $this->assertIsResource($result->getPublicKey()->getResource()); - $this->assertIsResource($result->getPrivateKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPublicKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPrivateKey()->getResource()); $details = openssl_pkey_get_details($result->getPrivateKey()->getResource()); $this->assertArrayHasKey('dh', $details); + + $this->assertEquals($details['key'], $result->getPublicKey()->getPEM()); } - public function test generateKeyPair generate random instance of KeyPair using DSA() + public function testGenerateKeyPairGenerateRandomInstanceOfKeyPairUsingDSA() { $result = $this->service->generateKeyPair(new DsaKeyOption(1024)); $this->assertInstanceOf(KeyPair::class, $result); - $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $result->getPublicKey()->getPEM()); - $this->assertStringContainsString('-----BEGIN PRIVATE KEY-----', $result->getPrivateKey()->getPEM()); - $this->assertIsResource($result->getPublicKey()->getResource()); - $this->assertIsResource($result->getPrivateKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPublicKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPrivateKey()->getResource()); $details = openssl_pkey_get_details($result->getPrivateKey()->getResource()); $this->assertEquals(1024, $details['bits']); $this->assertArrayHasKey('dsa', $details); + + $this->assertEquals($details['key'], $result->getPublicKey()->getPEM()); } /** * @requires PHP 7.1 */ - public function test generateKeyPair generate random instance of KeyPair using EC() + public function testGenerateKeyPairGenerateRandomInstanceOfKeyPairUsingEC() { $result = $this->service->generateKeyPair(new EcKeyOption('secp112r1')); $this->assertInstanceOf(KeyPair::class, $result); - $this->assertStringContainsString('-----BEGIN PUBLIC KEY-----', $result->getPublicKey()->getPEM()); - $this->assertStringContainsString('-----BEGIN EC PRIVATE KEY-----', $result->getPrivateKey()->getPEM()); - $this->assertIsResource($result->getPublicKey()->getResource()); - $this->assertIsResource($result->getPrivateKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPublicKey()->getResource()); + $this->assertIsOpenSslAsymmetricKey($result->getPrivateKey()->getResource()); $details = openssl_pkey_get_details($result->getPrivateKey()->getResource()); $this->assertEquals(112, $details['bits']); $this->assertArrayHasKey('ec', $details); $this->assertEquals('secp112r1', $details['ec']['curve_name']); + + $this->assertEquals($details['key'], $result->getPublicKey()->getPEM()); } } diff --git a/tests/Ssl/Parser/CertificateParserTest.php b/tests/Ssl/Parser/CertificateParserTest.php index 309d7ef5..7b82373e 100644 --- a/tests/Ssl/Parser/CertificateParserTest.php +++ b/tests/Ssl/Parser/CertificateParserTest.php @@ -21,20 +21,20 @@ class CertificateParserTest extends TestCase /** @var CertificateParser */ private $service; - public function setUp() + public function setUp(): void { parent::setUp(); $this->service = new CertificateParser(); } - public function test parse raise proper exception() + public function testParseRaiseProperException() { $this->expectException('AcmePhp\Ssl\Exception\CertificateParsingException'); $this->service->parse(new Certificate('Not a cert')); } - public function test parse returns instance of ParsedCertificate() + public function testParseReturnsInstanceOfParsedCertificate() { $result = $this->service->parse( new Certificate( @@ -88,7 +88,7 @@ public function test parse returns instance of ParsedCertificate() $this->assertFalse($result->isSelfSigned()); } - public function test parse without issuer CN returns instance of ParsedCertificate() + public function testParseWithoutIssuerCNReturnsInstanceOfParsedCertificate() { $result = $this->service->parse( new Certificate( diff --git a/tests/Ssl/Parser/KeyParserTest.php b/tests/Ssl/Parser/KeyParserTest.php index 6b0ad0d9..93e8a4e4 100644 --- a/tests/Ssl/Parser/KeyParserTest.php +++ b/tests/Ssl/Parser/KeyParserTest.php @@ -23,37 +23,37 @@ class KeyParserTest extends TestCase /** @var KeyParser */ private $service; - public function setUp() + public function setUp(): void { parent::setUp(); $this->service = new KeyParser(); } - public function test parse PublicKey raise proper exception() + public function testParsePublicKeyRaiseProperException() { $this->expectException('AcmePhp\Ssl\Exception\KeyParsingException'); $this->service->parse(new PublicKey('Not a key')); } - public function test parse PrivateKey raise proper exception() + public function testParsePrivateKeyRaiseProperException() { $this->expectException('AcmePhp\Ssl\Exception\KeyParsingException'); $this->service->parse(new PrivateKey('Not a key')); } - public function test get PrivateKey has invalid detail() + public function testGetPrivateKeyHasInvalidDetail() { $this->assertFalse($this->service->parse($this->getPrivateKey())->hasDetail('invalid')); } - public function test get PrivateKey get invalid detail raise proper exception() + public function testGetPrivateKeyGetInvalidDetailRaiseProperException() { $this->expectException('InvalidArgumentException'); $this->service->parse($this->getPrivateKey())->getDetail('invalid'); } - public function test parse PrivateKey returns instance of ParsedKey() + public function testParsePrivateKeyReturnsInstanceOfParsedKey() { $result = $this->service->parse($this->getPrivateKey()); @@ -67,7 +67,7 @@ public function test parse PrivateKey returns instance of ParsedKey() $this->assertEquals(trim($this->getPublicKey()->getPEM()), trim($result->getKey())); } - public function test parse PublicKey returns instance of ParsedKey() + public function testParsePublicKeyReturnsInstanceOfParsedKey() { $result = $this->service->parse($this->getPublicKey()); diff --git a/tests/Ssl/PrivateKeyTest.php b/tests/Ssl/PrivateKeyTest.php index 2bc80dfe..e59cbe50 100644 --- a/tests/Ssl/PrivateKeyTest.php +++ b/tests/Ssl/PrivateKeyTest.php @@ -17,7 +17,7 @@ class PrivateKeyTest extends TestCase { - public function test getPublicKey returns a PublicKey() + public function testGetPublicKeyReturnsAPublicKey() { $privateKey = new PrivateKey('-----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDH3IKV8sJZZHGd @@ -78,7 +78,7 @@ public function test getPublicKey returns a PublicKey() $this->assertEquals('80969771cf03d0331d1911810feff5fc', md5($publicKey->getPEM())); } - public function test fromDER returns a PrivateKey() + public function testFromDERReturnsAPrivateKey() { $derb64 = 'MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDH3IKV8sJZZHGd Q0vUN9GHJACixg8N1wFpUe763HmnWwiyCFHK9YjOfxkDSRK+2lP72Ns+RTBwtM8s @@ -137,7 +137,7 @@ public function test fromDER returns a PrivateKey() $this->assertEquals('a6dcb8eaae257961d2ee888899f087ef', md5($privateKey->getPEM())); } - public function test getDER returns a string() + public function testGetDERReturnsAString() { $privateKey = new PrivateKey('-----BEGIN PRIVATE KEY----- MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQDH3IKV8sJZZHGd diff --git a/tests/Ssl/PublicKeyTest.php b/tests/Ssl/PublicKeyTest.php index 35069b92..ac44e63a 100644 --- a/tests/Ssl/PublicKeyTest.php +++ b/tests/Ssl/PublicKeyTest.php @@ -16,7 +16,7 @@ class PublicKeyTest extends TestCase { - public function test fromDER returns a PublicKey() + public function testFromDERReturnsAPublicKey() { $derb64 = 'MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx9yClfLCWWRxnUNL1DfR hyQAosYPDdcBaVHu+tx5p1sIsghRyvWIzn8ZA0kSvtpT+9jbPkUwcLTPLGW0SAC8 @@ -37,7 +37,7 @@ public function test fromDER returns a PublicKey() $this->assertEquals('48fa4235a71c704c815363702d7effbb', md5($publicKey->getPEM())); } - public function test getDER returns a string() + public function testGetDERReturnsAString() { $publicKey = new PublicKey('-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx9yClfLCWWRxnUNL1DfR @@ -60,7 +60,7 @@ public function test getDER returns a string() $this->assertEquals('d2ea173bab74794037c74653b65433af', md5($der)); } - public function test getHPKP returns a string() + public function testGetHPKPReturnsAString() { $publicKey = new PublicKey('-----BEGIN PUBLIC KEY----- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAx9yClfLCWWRxnUNL1DfR diff --git a/tests/Ssl/Signer/CertificateRequestSignerTest.php b/tests/Ssl/Signer/CertificateRequestSignerTest.php index 779c3a0d..473d6e6b 100644 --- a/tests/Ssl/Signer/CertificateRequestSignerTest.php +++ b/tests/Ssl/Signer/CertificateRequestSignerTest.php @@ -23,14 +23,14 @@ class CertificateRequestSignerTest extends TestCase /** @var CertificateRequestSigner */ private $service; - public function setUp() + public function setUp(): void { parent::setUp(); $this->service = new CertificateRequestSigner(); } - public function test signCertificateRequest returns a certificate() + public function testSignCertificateRequestReturnsACertificate() { $dummyDistinguishedName = new DistinguishedName( 'acmephp.com', @@ -59,7 +59,7 @@ public function test signCertificateRequest returns a certificate() ); } - public function test signCertificateRequest use default values() + public function testSignCertificateRequestUseDefaultValues() { $dummyDistinguishedName = new DistinguishedName( 'acmephp.com' @@ -80,7 +80,7 @@ public function test signCertificateRequest use default values() ); } - public function test signCertificateRequest with subject alternative names() + public function testSignCertificateRequestWithSubjectAlternativeNames() { $dummyDistinguishedName = new DistinguishedName( 'acmephp.com', diff --git a/tests/Ssl/Signer/DataSignerTest.php b/tests/Ssl/Signer/DataSignerTest.php index 36ded63c..cf54d1a6 100644 --- a/tests/Ssl/Signer/DataSignerTest.php +++ b/tests/Ssl/Signer/DataSignerTest.php @@ -23,14 +23,14 @@ class DataSignerTest extends TestCase /** @var DataSigner */ private $service; - public function setUp() + public function setUp(): void { parent::setUp(); $this->service = new DataSigner(); } - public function test signData returns a signature() + public function testSignDataReturnsASignature() { $privateRsaKey = (new RsaKeyGenerator())->generatePrivateKey(new RsaKeyOption()); @@ -51,7 +51,7 @@ public function test signData returns a signature() /** * @requires PHP 7.1 */ - public function test signData returns a signature for ec keys() + public function testSignDataReturnsASignatureForEcKeys() { $this->assertEquals(64, \strlen($this->service->signData('foo', (new EcKeyGenerator())->generatePrivateKey(new EcKeyOption('prime256v1')), OPENSSL_ALGO_SHA256, DataSigner::FORMAT_ECDSA))); $this->assertEquals(96, \strlen($this->service->signData('foo', (new EcKeyGenerator())->generatePrivateKey(new EcKeyOption('secp384r1')), OPENSSL_ALGO_SHA384, DataSigner::FORMAT_ECDSA))); diff --git a/tests/run.sh b/tests/run.sh index d2c3ed8a..a5f3a4b5 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash +set -e +set -o pipefail + # Root directory -cd $( dirname "${BASH_SOURCE[0]}" ) -cd .. +cd $( dirname "${BASH_SOURCE[0]}" )/.. tempfile=$(mktemp) cert='-----BEGIN CERTIFICATE----- diff --git a/tests/setup.sh b/tests/setup.sh index 5ed674aa..7165af64 100755 --- a/tests/setup.sh +++ b/tests/setup.sh @@ -1,15 +1,19 @@ #!/usr/bin/env bash +set -e +set -o pipefail + # Root directory -cd $( dirname "${BASH_SOURCE[0]}" ) -cd .. +cd $( dirname "${BASH_SOURCE[0]}" )/.. # SFTP -docker run -d --name acme_sftp -p 8022:22 atmoz/sftp acmephp:acmephp:::share +docker run -d --rm --name acme_sftp -p 8022:22 atmoz/sftp acmephp:acmephp:::share # pebble -docker run -d --name acme_server --net host letsencrypt/pebble-challtestsrv pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 127.0.0.1 -docker run -d --name acme_pebble --net host -e PEBBLE_VA_NOSLEEP=1 -e PEBBLE_WFE_NONCEREJECT=0 letsencrypt/pebble pebble -dnsserver 127.0.0.1:8053 +MODE=${PEBBLE_MODE:-default} + +docker run -d --rm --name acme_server --net host letsencrypt/pebble-challtestsrv pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 127.0.0.1 +docker run -d --rm --name acme_pebble --net host -e PEBBLE_VA_NOSLEEP=1 -e PEBBLE_WFE_NONCEREJECT=0 -e PEBBLE_ALTERNATE_ROOTS=1 -v $(pwd)/tests/Fixtures/pebble-config-$MODE.json:/test/config/pebble-config.json letsencrypt/pebble pebble -dnsserver 127.0.0.1:8053 # Wait for boot to be completed docker run --rm --net host martin/wait -c localhost:14000,localhost:8022,localhost:8053,localhost:5002 -t 120