diff --git a/bin/acme b/bin/acme index be8da69..9f5cc89 100755 --- a/bin/acme +++ b/bin/acme @@ -1,6 +1,7 @@ #!/usr/bin/env php "Setup, issue and renew based on a single configuration file.", - "setup" => "Setup and register account.", - "issue" => "Issue a new certificate.", - "check" => "Check if a certificate is still valid long enough.", - "revoke" => "Revoke a certificate.", - "status" => "Show status about local certificates.", - "version" => "Print version information.", - "help" => "Print this help information.", + 'auto' => 'Setup, issue and renew based on a single configuration file.', + 'setup' => 'Setup and register account.', + 'issue' => 'Issue a new certificate.', + 'check' => 'Check if a certificate is still valid long enough.', + 'revoke' => 'Revoke a certificate.', + 'status' => 'Show status about local certificates.', + 'version' => 'Print version information.', + 'help' => 'Print this help information.', ]; $binary = \Kelunik\AcmeClient\getBinary(); @@ -75,22 +76,22 @@ EOT; $climate = new CLImate; -if (!in_array(PHP_SAPI, ["cli", "phpdbg"], true)) { - $climate->error("Please run this script on the command line!"); +if (!in_array(PHP_SAPI, ['cli', 'phpdbg'], true)) { + $climate->error('Please run this script on the command line!'); exit(1); } -if (PHP_VERSION_ID < 50600) { - $climate->yellow("You're using an older version of PHP which is no longer supported and will not even receive security fixes anymore. Have a look at http://php.net/supported-versions.php and upgrade now!"); +if (PHP_VERSION_ID < 70000) { + $climate->yellow("You're using an older version of PHP which is no longer supported by this client. Have a look at http://php.net/supported-versions.php and upgrade at least to PHP 7.0!"); $climate->br(2); } -if (count($argv) === 1 || in_array($argv[1], ["-h", "help", "--help"], true)) { +if (count($argv) === 1 || in_array($argv[1], ['-h', 'help', '--help'], true)) { $climate->out($logo . $help); exit(0); } -if (!in_array($argv[1], array_keys($commands))) { +if (!array_key_exists($argv[1], $commands)) { $climate->error("Unknown command '{$argv[1]}'. Use --help for a list of available commands."); $suggestion = \Kelunik\AcmeClient\suggestCommand($argv[1], array_keys($commands)); @@ -114,14 +115,14 @@ try { $climate->arguments->add($definition); - if (count($argv) === 3 && in_array($argv[2], ["-h", "--help"], true)) { + if (count($argv) === 3 && in_array($argv[2], ['-h', '--help'], true)) { $climate->usage(["{$binary} {$argv[1]}"]); $climate->br(); exit(0); - } else { - $climate->arguments->parse(array_values($args)); } + + $climate->arguments->parse(array_values($args)); } catch (Exception $e) { $climate->usage(["{$binary} {$argv[1]}"]); $climate->br(); @@ -135,16 +136,16 @@ try { $injector = new Injector; $injector->share($climate); $injector->share(new AcmeFactory); -$injector->share(new Amp\Artax\Client(new Amp\Artax\Cookie\NullCookieJar)); +$injector->share(new Amp\Artax\DefaultClient); $command = $injector->make($class); -Amp\run(function () use ($command, $climate) { +Loop::run(function () use ($command, $climate) { $handler = function ($e) use ($climate) { $error = (string) $e; $lines = explode("\n", $error); $lines = array_filter($lines, function ($line) { - return strlen($line) && $line[0] !== "#" && $line !== "Stack trace:"; + return $line !== '' && $line[0] !== '#' && $line !== 'Stack trace:'; }); foreach ($lines as $line) { @@ -155,7 +156,7 @@ Amp\run(function () use ($command, $climate) { }; try { - $exitCode = (yield $command->execute($climate->arguments)); + $exitCode = yield $command->execute($climate->arguments); if ($exitCode === null) { exit(0); @@ -164,9 +165,7 @@ Amp\run(function () use ($command, $climate) { exit($exitCode); } catch (Throwable $e) { $handler($e); - } catch (Exception $e) { - $handler($e); } - Amp\stop(); + Loop::stop(); }); diff --git a/composer.json b/composer.json index a86fe67..f12bf74 100644 --- a/composer.json +++ b/composer.json @@ -11,20 +11,20 @@ "tls" ], "require": { - "php": "^5.5|^7", + "php": ">=7", "ext-openssl": "*", - "amphp/process": "^0.1.1", - "kelunik/acme": "^0.3", + "amphp/process": "^0.2", + "kelunik/acme": "^0.5", "kelunik/certificate": "^1", - "league/climate": "^3", - "rdlowrey/auryn": "^1", - "webmozart/assert": "^1", + "league/climate": "^3.2", + "rdlowrey/auryn": "^1.4.2", + "webmozart/assert": "^1.2", "symfony/yaml": "^3.0" }, "require-dev": { - "phpunit/phpunit": "^4|^5", - "friendsofphp/php-cs-fixer": "^1.9", - "macfja/phar-builder": "^0.2.5" + "phpunit/phpunit": "^6", + "friendsofphp/php-cs-fixer": "^2.9", + "macfja/phar-builder": "^0.2.6" }, "license": "MIT", "authors": [ @@ -33,8 +33,6 @@ "email": "me@kelunik.com" } ], - "minimum-stability": "dev", - "prefer-stable": true, "autoload": { "psr-4": { "Kelunik\\AcmeClient\\": "src" @@ -43,12 +41,6 @@ "src/functions.php" ] }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/kelunik/pharbuilder" - } - ], "extra": { "phar-builder": { "compression": "GZip", diff --git a/composer.lock b/composer.lock index ea71c5d..40814e1 100644 --- a/composer.lock +++ b/composer.lock @@ -4,41 +4,45 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "content-hash": "9989cd58e3ed427161a28dc22d81993a", + "content-hash": "453e56fe6bb9de302a96b94f70baa09a", "packages": [ { "name": "amphp/amp", - "version": "v1.2.2", + "version": "v2.0.5", "source": { "type": "git", "url": "https://github.com/amphp/amp.git", - "reference": "4f2161da5f68f274f116985635aea63b5c0f54d2" + "reference": "502b4be0008ed2f9479ad598187e49683ce6ef39" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/amp/zipball/4f2161da5f68f274f116985635aea63b5c0f54d2", - "reference": "4f2161da5f68f274f116985635aea63b5c0f54d2", + "url": "https://api.github.com/repos/amphp/amp/zipball/502b4be0008ed2f9479ad598187e49683ce6ef39", + "reference": "502b4be0008ed2f9479ad598187e49683ce6ef39", "shasum": "" }, "require": { - "php": ">=5.5" + "php": ">=7" }, "require-dev": { - "fabpot/php-cs-fixer": "~1.9", - "phpunit/phpunit": "~4.8" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpstan/phpstan": "^0.8.5", + "phpunit/phpunit": "^6.0.9", + "react/promise": "^2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "2.0.x-dev" } }, "autoload": { "psr-4": { - "Amp\\": "lib/" + "Amp\\": "lib" }, "files": [ - "lib/functions.php" + "lib/functions.php", + "lib/Internal/functions.php" ] }, "notification-url": "https://packagist.org/downloads/", @@ -46,56 +50,70 @@ "MIT" ], "authors": [ + { + "name": "Bob Weinand", + "email": "bobwei9@hotmail.com" + }, + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, { "name": "Daniel Lowrey", - "email": "rdlowrey@php.net", - "role": "Creator / Lead Developer" + "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "A non-blocking concurrency framework for PHP applications", - "homepage": "https://github.com/amphp/amp", + "description": "A non-blocking concurrency framework for PHP applications.", + "homepage": "http://amphp.org/amp", "keywords": [ "async", + "asynchronous", + "awaitable", "concurrency", "event", + "event-loop", + "future", "non-blocking", "promise" ], - "time": "2016-05-12T12:54:59+00:00" + "time": "2017-12-19T17:27:18+00:00" }, { "name": "amphp/artax", - "version": "v2.0.6", + "version": "v3.0.13", "source": { "type": "git", "url": "https://github.com/amphp/artax.git", - "reference": "8ed34eb97685432b603fdf5b9279bd908333c814" + "reference": "340bd11600b5c77a2ab136d4300a776a02a85cc9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/artax/zipball/8ed34eb97685432b603fdf5b9279bd908333c814", - "reference": "8ed34eb97685432b603fdf5b9279bd908333c814", + "url": "https://api.github.com/repos/amphp/artax/zipball/340bd11600b5c77a2ab136d4300a776a02a85cc9", + "reference": "340bd11600b5c77a2ab136d4300a776a02a85cc9", "shasum": "" }, "require": { - "amphp/amp": "^1", - "amphp/socket": "^0.9", - "php": ">=5.5.0" + "amphp/amp": "^2", + "amphp/byte-stream": "^1.1.6", + "amphp/file": "^0.2 || ^0.3", + "amphp/socket": "^0.10", + "amphp/uri": "^0.1", + "kelunik/certificate": "^1.1", + "php": ">=7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~1.9", - "phpunit/phpunit": "~4.8" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.x-dev", - "dev-1.x": "1.x-dev" - } - }, "autoload": { "psr-4": { - "Amp\\Artax\\": "lib/" + "Amp\\Artax\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -103,10 +121,13 @@ "MIT" ], "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, { "name": "Daniel Lowrey", - "email": "rdlowrey@gmail.com", - "role": "Creator / Lead Developer" + "email": "rdlowrey@gmail.com" } ], "description": "Asynchronous parallel HTTP/1.1 client built on the Amp concurrency framework", @@ -119,36 +140,91 @@ "parallel", "rest" ], - "time": "2017-05-09T19:47:08+00:00" + "time": "2017-12-19T13:45:05+00:00" }, { - "name": "amphp/cache", - "version": "v0.1.0", + "name": "amphp/byte-stream", + "version": "v1.2.1", "source": { "type": "git", - "url": "https://github.com/amphp/cache.git", - "reference": "26709c198dcee686557801eda6d9345f3cfa8874" + "url": "https://github.com/amphp/byte-stream.git", + "reference": "0c059ba0e6aa7a1ebdce0ba92c77be1ec5dd9143" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/cache/zipball/26709c198dcee686557801eda6d9345f3cfa8874", - "reference": "26709c198dcee686557801eda6d9345f3cfa8874", + "url": "https://api.github.com/repos/amphp/byte-stream/zipball/0c059ba0e6aa7a1ebdce0ba92c77be1ec5dd9143", + "reference": "0c059ba0e6aa7a1ebdce0ba92c77be1ec5dd9143", "shasum": "" }, "require": { - "amphp/amp": "^1" + "amphp/amp": "^2" }, "require-dev": { - "fabpot/php-cs-fixer": "~1.9", - "phpunit/phpunit": "~4.8" - }, - "suggest": { - "amphp/redis": "For redis cache driver support" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" }, "type": "library", "autoload": { "psr-4": { - "Amp\\Cache\\": "lib/" + "Amp\\ByteStream\\": "lib" + }, + "files": [ + "lib/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "A stream abstraction to make working with non-blocking I/O simple.", + "homepage": "http://amphp.org/byte-stream", + "keywords": [ + "amp", + "amphp", + "async", + "io", + "non-blocking", + "stream" + ], + "time": "2017-12-10T16:40:58+00:00" + }, + { + "name": "amphp/cache", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/cache.git", + "reference": "ab2339e465d9d383dc748f288d530fd7cd7aadea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/cache/zipball/ab2339e465d9d383dc748f288d530fd7cd7aadea", + "reference": "ab2339e465d9d383dc748f288d530fd7cd7aadea", + "shasum": "" + }, + "require": { + "amphp/amp": "^2" + }, + "require-dev": { + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Cache\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -165,36 +241,39 @@ "email": "rdlowrey@php.net" } ], - "description": "A promise-aware caching API built on the amp concurrency framework", + "description": "A promise-aware caching API for Amp.", "homepage": "https://github.com/amphp/cache", - "time": "2015-09-08T22:26:20+00:00" + "time": "2017-10-04T19:22:12+00:00" }, { "name": "amphp/dns", - "version": "v0.8.14", + "version": "v0.9.11", "source": { "type": "git", "url": "https://github.com/amphp/dns.git", - "reference": "5fc1cde2d29e94d731ab96d5ef5f9f20958315cc" + "reference": "16e6d6c22f293370a8d6120744aa04181cd92766" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/5fc1cde2d29e94d731ab96d5ef5f9f20958315cc", - "reference": "5fc1cde2d29e94d731ab96d5ef5f9f20958315cc", + "url": "https://api.github.com/repos/amphp/dns/zipball/16e6d6c22f293370a8d6120744aa04181cd92766", + "reference": "16e6d6c22f293370a8d6120744aa04181cd92766", "shasum": "" }, "require": { - "amphp/amp": "^1", - "amphp/cache": "^0.1", - "amphp/file": "^0.1", - "amphp/windows-registry": "^0.2.2", + "amphp/amp": "^2", + "amphp/byte-stream": "^1.1", + "amphp/cache": "^1.2", + "amphp/file": "^0.2 || ^0.3", + "amphp/parser": "^1", + "amphp/uri": "^0.1", + "amphp/windows-registry": "^0.3", "daverandom/libdns": "^1", - "php": ">=5.5" + "php": ">=7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^1.9", - "phpunit/php-code-coverage": ">=2.2", - "phpunit/phpunit": "^4.8|^5.1.3" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" }, "type": "library", "autoload": { @@ -202,7 +281,6 @@ "Amp\\Dns\\": "lib" }, "files": [ - "lib/constants.php", "lib/functions.php" ] }, @@ -226,48 +304,49 @@ { "name": "Chris Wright", "email": "addr@daverandom.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "Async DNS resolution built on the amp concurrency framework", + "description": "Async DNS resolution for Amp.", "homepage": "https://github.com/amphp/dns", "keywords": [ "amp", + "amphp", "async", "client", "dns", "resolve" ], - "time": "2017-02-05T22:17:40+00:00" + "time": "2017-12-16T18:59:47+00:00" }, { "name": "amphp/file", - "version": "v0.1.3", + "version": "v0.3.0", "source": { "type": "git", "url": "https://github.com/amphp/file.git", - "reference": "6612ae6757d4719492ed8b34ea6181ff67cfbed1" + "reference": "fb58fe8dd5d1eeae5af152bf9e37731de5423caf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/file/zipball/6612ae6757d4719492ed8b34ea6181ff67cfbed1", - "reference": "6612ae6757d4719492ed8b34ea6181ff67cfbed1", + "url": "https://api.github.com/repos/amphp/file/zipball/fb58fe8dd5d1eeae5af152bf9e37731de5423caf", + "reference": "fb58fe8dd5d1eeae5af152bf9e37731de5423caf", "shasum": "" }, "require": { - "amphp/amp": "^1", - "php": ">=5.5" + "amphp/amp": "^2", + "amphp/byte-stream": "^1", + "amphp/parallel": "^0.2" }, "require-dev": { - "friendsofphp/php-cs-fixer": "~1.9", - "phpunit/phpunit": "~4.8" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1.0-dev", - "dev-amp_v2": "0.2.0-dev" - } - }, "autoload": { "psr-4": { "Amp\\File\\": "lib" @@ -281,12 +360,20 @@ "MIT" ], "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, { "name": "Daniel Lowrey", "email": "rdlowrey@php.net" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "An async filesystem library built on the amp concurrency framework", + "description": "Allows non-blocking access to the filesystem for Amp.", "homepage": "https://github.com/amphp/file", "keywords": [ "amp", @@ -294,81 +381,268 @@ "async", "disk", "file", + "filesystem", + "io", "non-blocking", "static" ], - "time": "2016-10-01T17:43:52+00:00" + "time": "2017-12-15T04:36:16+00:00" }, { - "name": "amphp/process", - "version": "v0.1.3", + "name": "amphp/parallel", + "version": "v0.2.1", "source": { "type": "git", - "url": "https://github.com/amphp/process.git", - "reference": "f22cca2af36e442b771c0de2e24e8025550d8ffc" + "url": "https://github.com/amphp/parallel.git", + "reference": "0d40e6da980d56de8bbd292668becef6ed45a2a7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/process/zipball/f22cca2af36e442b771c0de2e24e8025550d8ffc", - "reference": "f22cca2af36e442b771c0de2e24e8025550d8ffc", + "url": "https://api.github.com/repos/amphp/parallel/zipball/0d40e6da980d56de8bbd292668becef6ed45a2a7", + "reference": "0d40e6da980d56de8bbd292668becef6ed45a2a7", "shasum": "" }, "require": { - "amphp/amp": "^1" + "amphp/amp": "^2", + "amphp/byte-stream": "^1.2", + "amphp/parser": "^1", + "amphp/process": "^0.2 || ^0.3", + "amphp/sync": "^1.0.1" }, "require-dev": { - "fabpot/php-cs-fixer": "~1.9", - "phpunit/phpunit": "^4.8" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" + }, + "suggest": { + "ext-pthreads": "Required for thread contexts" }, "type": "library", "autoload": { - "classmap": [ - { - "Amp\\Process": "Process.php" - } + "psr-4": { + "Amp\\Parallel\\": "lib" + }, + "files": [ + "lib/Worker/functions.php" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], + "authors": [ + { + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Parallel processing component for Amp.", + "homepage": "https://github.com/amphp/parallel", + "keywords": [ + "async", + "asynchronous", + "concurrent", + "multi-processing", + "multi-threading" + ], + "time": "2017-12-27T18:36:28+00:00" + }, + { + "name": "amphp/parser", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://github.com/amphp/parser.git", + "reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/parser/zipball/f83e68f03d5b8e8e0365b8792985a7f341c57ae1", + "reference": "f83e68f03d5b8e8e0365b8792985a7f341c57ae1", + "shasum": "" + }, + "require": { + "php": ">=7" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Parser\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "A generator parser to make streaming parsers simple.", + "homepage": "https://github.com/amphp/parser", + "keywords": [ + "async", + "non-blocking", + "parser", + "stream" + ], + "time": "2017-06-06T05:29:10+00:00" + }, + { + "name": "amphp/process", + "version": "v0.2.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/process.git", + "reference": "5aa6040fcf5c98bfb4f4a8e68305cb6cd6a3d37a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/process/zipball/5aa6040fcf5c98bfb4f4a8e68305cb6cd6a3d37a", + "reference": "5aa6040fcf5c98bfb4f4a8e68305cb6cd6a3d37a", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/byte-stream": "^1" + }, + "require-dev": { + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "kelunik/fqn-check": "^0.1.3", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Process\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], "authors": [ { "name": "Bob Weinand", "email": "bobwei9@hotmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], "description": "Asynchronous process manager", "homepage": "https://github.com/amphp/process", - "time": "2016-09-24T10:49:26+00:00" + "time": "2017-07-18T03:37:19+00:00" }, { "name": "amphp/socket", - "version": "v0.9.9", + "version": "v0.10.5", "source": { "type": "git", "url": "https://github.com/amphp/socket.git", - "reference": "722614608c1de7099661187fad4e15c876816db1" + "reference": "937aff6eaa08cd4da03d475d98bcf911bbb854ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/722614608c1de7099661187fad4e15c876816db1", - "reference": "722614608c1de7099661187fad4e15c876816db1", + "url": "https://api.github.com/repos/amphp/socket/zipball/937aff6eaa08cd4da03d475d98bcf911bbb854ab", + "reference": "937aff6eaa08cd4da03d475d98bcf911bbb854ab", "shasum": "" }, "require": { - "amphp/amp": "^1", - "amphp/dns": "^0.8", - "php": ">=5.5" + "amphp/amp": "^2", + "amphp/byte-stream": "^1.1", + "amphp/dns": "^0.9", + "amphp/uri": "^0.1", + "php": ">=7.0" }, "require-dev": { - "fabpot/php-cs-fixer": "~1.9", - "phpunit/phpunit": "~4.8" + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" }, "type": "library", "autoload": { "psr-4": { - "Amp\\Socket\\": "lib/" + "Amp\\Socket\\": "lib" + }, + "files": [ + "lib/functions.php", + "lib/Internal/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey", + "email": "rdlowrey@gmail.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" + } + ], + "description": "Async socket connection / server tools for Amp.", + "homepage": "https://github.com/amphp/socket", + "keywords": [ + "amp", + "async", + "encryption", + "non-blocking", + "sockets", + "tcp", + "tls" + ], + "time": "2017-12-19T18:19:46+00:00" + }, + { + "name": "amphp/sync", + "version": "v1.0.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/sync.git", + "reference": "a1d8f244eb19e3e2a96abc4686cebc80995bbc90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/sync/zipball/a1d8f244eb19e3e2a96abc4686cebc80995bbc90", + "reference": "a1d8f244eb19e3e2a96abc4686cebc80995bbc90", + "shasum": "" + }, + "require": { + "amphp/amp": "^2" + }, + "require-dev": { + "amphp/phpunit-util": "^1", + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\Sync\\": "lib" }, "files": [ "lib/functions.php" @@ -380,46 +654,94 @@ ], "authors": [ { - "name": "Daniel Lowrey", - "email": "rdlowrey@gmail.com" + "name": "Stephen Coakley", + "email": "me@stephencoakley.com" + }, + { + "name": "Aaron Piotrowski", + "email": "aaron@trowski.com" } ], - "description": "Async socket connection tools for the amp concurrency framework", - "homepage": "https://github.com/amphp/socket", + "description": "Mutex, Semaphore, and other synchronization tools for Amp.", + "homepage": "https://github.com/amphp/sync", "keywords": [ - "amp", "async", - "encryption", - "non-blocking", - "sockets", - "tcp", - "tls" + "asynchronous", + "mutex", + "semaphore", + "synchronization" ], - "time": "2016-07-18T22:03:24+00:00" + "time": "2017-11-29T21:48:53+00:00" }, { - "name": "amphp/windows-registry", - "version": "v0.2.2", + "name": "amphp/uri", + "version": "v0.1.3", "source": { "type": "git", - "url": "https://github.com/amphp/windows-registry.git", - "reference": "e4420eb368008c8fe81c0b481506306272cc3d21" + "url": "https://github.com/amphp/uri.git", + "reference": "b857ba4df3cf0852302ba1637fccce4ce1205241" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/windows-registry/zipball/e4420eb368008c8fe81c0b481506306272cc3d21", - "reference": "e4420eb368008c8fe81c0b481506306272cc3d21", + "url": "https://api.github.com/repos/amphp/uri/zipball/b857ba4df3cf0852302ba1637fccce4ce1205241", + "reference": "b857ba4df3cf0852302ba1637fccce4ce1205241", "shasum": "" }, - "require": { - "amphp/amp": "^1.2", - "amphp/process": "^0.1.3", - "php": ">=5.5" + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.3", + "phpunit/phpunit": "^6" }, "type": "library", "autoload": { "psr-4": { - "Amp\\WindowsRegistry\\": "src" + "Amp\\Uri\\": "src" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Niklas Keller", + "email": "me@kelunik.com" + }, + { + "name": "Daniel Lowrey" + } + ], + "description": "Uri Parser and Resolver.", + "homepage": "https://github.com/amphp/uri", + "time": "2017-10-23T12:40:35+00:00" + }, + { + "name": "amphp/windows-registry", + "version": "v0.3.1", + "source": { + "type": "git", + "url": "https://github.com/amphp/windows-registry.git", + "reference": "46ba1463dfffc8081b4b483fac05d3f51ecb1d87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/amphp/windows-registry/zipball/46ba1463dfffc8081b4b483fac05d3f51ecb1d87", + "reference": "46ba1463dfffc8081b4b483fac05d3f51ecb1d87", + "shasum": "" + }, + "require": { + "amphp/amp": "^2", + "amphp/process": "^0.2 || ^0.3" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^2.3" + }, + "type": "library", + "autoload": { + "psr-4": { + "Amp\\WindowsRegistry\\": "lib" } }, "notification-url": "https://packagist.org/downloads/", @@ -433,7 +755,7 @@ } ], "description": "Windows Registry Reader.", - "time": "2017-01-04T23:52:31+00:00" + "time": "2017-12-11T08:35:51+00:00" }, { "name": "daverandom/libdns", @@ -470,38 +792,41 @@ }, { "name": "kelunik/acme", - "version": "v0.3.3", + "version": "v0.5.0", "source": { "type": "git", "url": "https://github.com/kelunik/acme.git", - "reference": "69a94a00e6c02f57a051ec962bdd1fc9649a96dd" + "reference": "496c9189f37ab512664cb12dc7b994c1ca6965cb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kelunik/acme/zipball/69a94a00e6c02f57a051ec962bdd1fc9649a96dd", - "reference": "69a94a00e6c02f57a051ec962bdd1fc9649a96dd", + "url": "https://api.github.com/repos/kelunik/acme/zipball/496c9189f37ab512664cb12dc7b994c1ca6965cb", + "reference": "496c9189f37ab512664cb12dc7b994c1ca6965cb", "shasum": "" }, "require": { - "amphp/artax": "^2", - "namshi/jose": "^6", + "amphp/amp": "^2", + "amphp/artax": "^3", + "namshi/jose": "^7", "sabre/uri": "^1" }, "require-dev": { - "fabpot/php-cs-fixer": "^1.9", - "phpdocumentor/phpdocumentor": "^2", - "phpunit/phpunit": "^5" + "amphp/phpunit-util": "^1.0", + "phpunit/phpunit": "^6" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "0.3-dev" + "dev-master": "0.5-dev" } }, "autoload": { "psr-4": { "Kelunik\\Acme\\": "lib" - } + }, + "files": [ + "lib/functions.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -523,20 +848,20 @@ "ssl", "tls" ], - "time": "2016-03-23T19:49:15+00:00" + "time": "2017-11-28T14:26:01+00:00" }, { "name": "kelunik/certificate", - "version": "v1.0.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/kelunik/certificate.git", - "reference": "b4ea243fbedc56813c74ba5b00bf8c69c6892a80" + "reference": "524fa432ed1b5f50efbe7749f3c19e28a9866bc7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/kelunik/certificate/zipball/b4ea243fbedc56813c74ba5b00bf8c69c6892a80", - "reference": "b4ea243fbedc56813c74ba5b00bf8c69c6892a80", + "url": "https://api.github.com/repos/kelunik/certificate/zipball/524fa432ed1b5f50efbe7749f3c19e28a9866bc7", + "reference": "524fa432ed1b5f50efbe7749f3c19e28a9866bc7", "shasum": "" }, "require": { @@ -572,7 +897,7 @@ "pem", "x509" ], - "time": "2016-01-27T08:46:30+00:00" + "time": "2017-07-04T19:37:30+00:00" }, { "name": "league/climate", @@ -627,33 +952,36 @@ }, { "name": "namshi/jose", - "version": "6.1.1", + "version": "7.2.3", "source": { "type": "git", "url": "https://github.com/namshi/jose.git", - "reference": "d234ab5da058bda234efbfc231a4ff68f9c984be" + "reference": "89a24d7eb3040e285dd5925fcad992378b82bcff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/namshi/jose/zipball/d234ab5da058bda234efbfc231a4ff68f9c984be", - "reference": "d234ab5da058bda234efbfc231a4ff68f9c984be", + "url": "https://api.github.com/repos/namshi/jose/zipball/89a24d7eb3040e285dd5925fcad992378b82bcff", + "reference": "89a24d7eb3040e285dd5925fcad992378b82bcff", "shasum": "" }, "require": { "ext-date": "*", "ext-hash": "*", "ext-json": "*", - "ext-openssl": "*", "ext-pcre": "*", "ext-spl": "*", "php": ">=5.5", - "phpseclib/phpseclib": "^2.0", "symfony/polyfill-php56": "^1.0" }, "require-dev": { + "phpseclib/phpseclib": "^2.0", "phpunit/phpunit": "^4.5|^5.0", "satooshi/php-coveralls": "^1.0" }, + "suggest": { + "ext-openssl": "Allows to use OpenSSL as crypto engine.", + "phpseclib/phpseclib": "Allows to use Phpseclib as crypto engine, use version ^2.0." + }, "type": "library", "autoload": { "psr-4": { @@ -683,112 +1011,20 @@ "jwt", "token" ], - "time": "2016-01-24T11:10:26+00:00" - }, - { - "name": "phpseclib/phpseclib", - "version": "2.0.5", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "f8dd0e18d2328c447dd4190fecd11ef52680d968" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f8dd0e18d2328c447dd4190fecd11ef52680d968", - "reference": "f8dd0e18d2328c447dd4190fecd11ef52680d968", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phing/phing": "~2.7", - "phpunit/phpunit": "~4.0", - "sami/sami": "~2.0", - "squizlabs/php_codesniffer": "~2.0" - }, - "suggest": { - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.", - "ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations." - }, - "type": "library", - "autoload": { - "files": [ - "phpseclib/bootstrap.php" - ], - "psr-4": { - "phpseclib\\": "phpseclib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, - { - "name": "Patrick Monnerat", - "email": "pm@datasphere.ch", - "role": "Developer" - }, - { - "name": "Andreas Fischer", - "email": "bantu@phpbb.com", - "role": "Developer" - }, - { - "name": "Hans-Jürgen Petrich", - "email": "petrich@tronic-media.com", - "role": "Developer" - }, - { - "name": "Graham Campbell", - "email": "graham@alt-three.com", - "role": "Developer" - } - ], - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", - "homepage": "http://phpseclib.sourceforge.net", - "keywords": [ - "BigInteger", - "aes", - "asn.1", - "asn1", - "blowfish", - "crypto", - "cryptography", - "encryption", - "rsa", - "security", - "sftp", - "signature", - "signing", - "ssh", - "twofish", - "x.509", - "x509" - ], - "time": "2017-05-08T05:58:35+00:00" + "time": "2016-12-05T07:27:31+00:00" }, { "name": "rdlowrey/auryn", - "version": "v1.4.1", + "version": "v1.4.2", "source": { "type": "git", "url": "https://github.com/rdlowrey/auryn.git", - "reference": "c6c92ec081c707f7e9e0a69185665147b30d66de" + "reference": "8c4dc07943599ba84f4f89eab8cf43efeef80395" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rdlowrey/auryn/zipball/c6c92ec081c707f7e9e0a69185665147b30d66de", - "reference": "c6c92ec081c707f7e9e0a69185665147b30d66de", + "url": "https://api.github.com/repos/rdlowrey/auryn/zipball/8c4dc07943599ba84f4f89eab8cf43efeef80395", + "reference": "8c4dc07943599ba84f4f89eab8cf43efeef80395", "shasum": "" }, "require": { @@ -836,19 +1072,19 @@ "dic", "ioc" ], - "time": "2017-04-07T16:12:28+00:00" + "time": "2017-05-15T06:26:46+00:00" }, { "name": "sabre/uri", "version": "1.2.1", "source": { "type": "git", - "url": "https://github.com/fruux/sabre-uri.git", + "url": "https://github.com/sabre-io/uri.git", "reference": "ada354d83579565949d80b2e15593c2371225e61" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruux/sabre-uri/zipball/ada354d83579565949d80b2e15593c2371225e61", + "url": "https://api.github.com/repos/sabre-io/uri/zipball/ada354d83579565949d80b2e15593c2371225e61", "reference": "ada354d83579565949d80b2e15593c2371225e61", "shasum": "" }, @@ -939,16 +1175,16 @@ }, { "name": "symfony/polyfill-php56", - "version": "v1.3.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php56.git", - "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c" + "reference": "265fc96795492430762c29be291a371494ba3a5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/1dd42b9b89556f18092f3d1ada22cb05ac85383c", - "reference": "1dd42b9b89556f18092f3d1ada22cb05ac85383c", + "url": "https://api.github.com/repos/symfony/polyfill-php56/zipball/265fc96795492430762c29be291a371494ba3a5b", + "reference": "265fc96795492430762c29be291a371494ba3a5b", "shasum": "" }, "require": { @@ -958,7 +1194,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -991,20 +1227,20 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/polyfill-util", - "version": "v1.3.0", + "version": "v1.6.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-util.git", - "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb" + "reference": "6e719200c8e540e0c0effeb31f96bdb344b94176" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/746bce0fca664ac0a575e465f65c6643faddf7fb", - "reference": "746bce0fca664ac0a575e465f65c6643faddf7fb", + "url": "https://api.github.com/repos/symfony/polyfill-util/zipball/6e719200c8e540e0c0effeb31f96bdb344b94176", + "reference": "6e719200c8e540e0c0effeb31f96bdb344b94176", "shasum": "" }, "require": { @@ -1013,7 +1249,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -1043,27 +1279,30 @@ "polyfill", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { "name": "symfony/yaml", - "version": "v3.2.8", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6" + "reference": "afe0cd38486505c9703707707d91450cfc1bd536" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6", - "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6", + "url": "https://api.github.com/repos/symfony/yaml/zipball/afe0cd38486505c9703707707d91450cfc1bd536", + "reference": "afe0cd38486505c9703707707d91450cfc1bd536", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^5.5.9|>=7.0.8" + }, + "conflict": { + "symfony/console": "<3.4" }, "require-dev": { - "symfony/console": "~2.8|~3.0" + "symfony/console": "~3.4|~4.0" }, "suggest": { "symfony/console": "For validating YAML files using the lint command" @@ -1071,7 +1310,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -1098,7 +1337,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-12-11T20:38:23+00:00" }, { "name": "webmozart/assert", @@ -1153,33 +1392,163 @@ ], "packages-dev": [ { - "name": "doctrine/instantiator", - "version": "1.0.5", + "name": "composer/semver", + "version": "1.4.2", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + "url": "https://github.com/composer/semver.git", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", - "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "url": "https://api.github.com/repos/composer/semver/zipball/c7cb9a2095a074d131b65a8a0cd294479d785573", + "reference": "c7cb9a2095a074d131b65a8a0cd294479d785573", "shasum": "" }, "require": { - "php": ">=5.3,<8.0-DEV" + "php": "^5.3.2 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.5 || ^5.0.5", + "phpunit/phpunit-mock-objects": "2.3.0 || ^3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "time": "2016-08-30T16:08:34+00:00" + }, + { + "name": "doctrine/annotations", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/annotations.git", + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "reference": "c7f2050c68a9ab0bdb0f98567ec08d80ea7d24d5", + "shasum": "" + }, + "require": { + "doctrine/lexer": "1.*", + "php": "^7.1" + }, + "require-dev": { + "doctrine/cache": "1.*", + "phpunit/phpunit": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Benjamin Eberlei", + "email": "kontakt@beberlei.de" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Jonathan Wage", + "email": "jonwage@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Docblock Annotations Parser", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "annotations", + "docblock", + "parser" + ], + "time": "2017-12-06T07:11:42+00:00" + }, + { + "name": "doctrine/instantiator", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda", + "shasum": "" + }, + "require": { + "php": "^7.1" }, "require-dev": { "athletic/athletic": "~0.1.8", "ext-pdo": "*", "ext-phar": "*", - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~2.0" + "phpunit/phpunit": "^6.2.3", + "squizlabs/php_codesniffer": "^3.0.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -1204,39 +1573,109 @@ "constructor", "instantiate" ], - "time": "2015-06-14T21:17:01+00:00" + "time": "2017-07-22T11:58:36+00:00" }, { - "name": "friendsofphp/php-cs-fixer", - "version": "v1.13.1", + "name": "doctrine/lexer", + "version": "v1.0.1", "source": { "type": "git", - "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", - "reference": "0ea4f7ed06ca55da1d8fc45da26ff87f261c4088" + "url": "https://github.com/doctrine/lexer.git", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/0ea4f7ed06ca55da1d8fc45da26ff87f261c4088", - "reference": "0ea4f7ed06ca55da1d8fc45da26ff87f261c4088", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", + "reference": "83893c552fd2045dd78aef794c31e694c37c0b8c", "shasum": "" }, "require": { + "php": ">=5.3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Doctrine\\Common\\Lexer\\": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Borschel", + "email": "roman@code-factory.org" + }, + { + "name": "Guilherme Blanco", + "email": "guilhermeblanco@gmail.com" + }, + { + "name": "Johannes Schmitt", + "email": "schmittjoh@gmail.com" + } + ], + "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", + "homepage": "http://www.doctrine-project.org", + "keywords": [ + "lexer", + "parser" + ], + "time": "2014-09-09T13:34:57+00:00" + }, + { + "name": "friendsofphp/php-cs-fixer", + "version": "v2.9.0", + "source": { + "type": "git", + "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git", + "reference": "454ddbe65da6a9297446f442bad244e8a99a9a38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/454ddbe65da6a9297446f442bad244e8a99a9a38", + "reference": "454ddbe65da6a9297446f442bad244e8a99a9a38", + "shasum": "" + }, + "require": { + "composer/semver": "^1.4", + "doctrine/annotations": "^1.2", + "ext-json": "*", "ext-tokenizer": "*", - "php": "^5.3.6 || >=7.0 <7.2", - "sebastian/diff": "^1.1", - "symfony/console": "^2.3 || ^3.0", - "symfony/event-dispatcher": "^2.1 || ^3.0", - "symfony/filesystem": "^2.1 || ^3.0", - "symfony/finder": "^2.1 || ^3.0", - "symfony/process": "^2.3 || ^3.0", - "symfony/stopwatch": "^2.5 || ^3.0" + "gecko-packages/gecko-php-unit": "^2.0 || ^3.0", + "php": "^5.6 || >=7.0 <7.3", + "php-cs-fixer/diff": "^1.2", + "symfony/console": "^3.2 || ^4.0", + "symfony/event-dispatcher": "^3.0 || ^4.0", + "symfony/filesystem": "^3.0 || ^4.0", + "symfony/finder": "^3.0 || ^4.0", + "symfony/options-resolver": "^3.0 || ^4.0", + "symfony/polyfill-php70": "^1.0", + "symfony/polyfill-php72": "^1.4", + "symfony/process": "^3.0 || ^4.0", + "symfony/stopwatch": "^3.0 || ^4.0" }, "conflict": { - "hhvm": "<3.9" + "hhvm": "*" }, "require-dev": { - "phpunit/phpunit": "^4.5|^5", - "satooshi/php-coveralls": "^1.0" + "johnkary/phpunit-speedtrap": "^1.1 || ^2.0@dev", + "justinrainbow/json-schema": "^5.0", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.0", + "php-cs-fixer/accessible-object": "^1.0", + "phpunit/phpunit": "^5.7.23 || ^6.4.3", + "symfony/phpunit-bridge": "^3.2.2 || ^4.0" + }, + "suggest": { + "ext-mbstring": "For handling non-UTF8 characters in cache signature.", + "symfony/polyfill-mbstring": "When enabling `ext-mbstring` is not possible." }, "bin": [ "php-cs-fixer" @@ -1244,8 +1683,15 @@ "type": "application", "autoload": { "psr-4": { - "Symfony\\CS\\": "Symfony/CS/" - } + "PhpCsFixer\\": "src/" + }, + "classmap": [ + "tests/Test/Assert/AssertTokensTrait.php", + "tests/Test/AbstractFixerTestCase.php", + "tests/Test/AbstractIntegrationTestCase.php", + "tests/Test/IntegrationCase.php", + "tests/Test/IntegrationCaseFactory.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1262,7 +1708,56 @@ } ], "description": "A tool to automatically fix PHP code style", - "time": "2016-12-01T00:05:05+00:00" + "time": "2017-12-08T16:36:20+00:00" + }, + { + "name": "gecko-packages/gecko-php-unit", + "version": "v3.0", + "source": { + "type": "git", + "url": "https://github.com/GeckoPackages/GeckoPHPUnit.git", + "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/GeckoPackages/GeckoPHPUnit/zipball/6a866551dffc2154c1b091bae3a7877d39c25ca3", + "reference": "6a866551dffc2154c1b091bae3a7877d39c25ca3", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "suggest": { + "ext-dom": "When testing with xml.", + "ext-libxml": "When testing with xml.", + "phpunit/phpunit": "This is an extension for it so make sure you have it some way." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "GeckoPackages\\PHPUnit\\": "src/PHPUnit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Additional PHPUnit asserts and constraints.", + "homepage": "https://github.com/GeckoPackages", + "keywords": [ + "extension", + "filesystem", + "phpunit" + ], + "time": "2017-08-23T07:46:41+00:00" }, { "name": "league/event", @@ -1316,25 +1811,25 @@ }, { "name": "macfja/phar-builder", - "version": "0.2.5", + "version": "0.2.6", "source": { "type": "git", "url": "https://github.com/MacFJA/PharBuilder.git", - "reference": "0a5270b565242a99371ff94d2ebecb85b2854a1d" + "reference": "ab3d6f2089e1dc908b545627ab857861bdf2891c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MacFJA/PharBuilder/zipball/0a5270b565242a99371ff94d2ebecb85b2854a1d", - "reference": "0a5270b565242a99371ff94d2ebecb85b2854a1d", + "url": "https://api.github.com/repos/MacFJA/PharBuilder/zipball/ab3d6f2089e1dc908b545627ab857861bdf2891c", + "reference": "ab3d6f2089e1dc908b545627ab857861bdf2891c", "shasum": "" }, "require": { "league/event": "^2.1", - "macfja/symfony-console-filechooser": "0.1", + "macfja/symfony-console-filechooser": "~0.2", "neutron/signal-handler": "^1.0", "php": ">=5.4.0", "rych/bytesize": "~1.0", - "symfony/console": "~2.7", + "symfony/console": "~2.7 || ^3.0", "symfony/process": "^2.7 || ^3.0", "webignition/readable-duration": "^0.2" }, @@ -1350,6 +1845,8 @@ "type": "library", "extra": { "phar-builder": { + "skip-shebang": false, + "include-dev": false, "entry-point": "bin/phar-builder.php", "compression": "None", "name": "phar-builder.phar", @@ -1374,25 +1871,25 @@ } ], "description": "CLI tool for create phar of your composer based project", - "time": "2016-08-07T12:35:14+00:00" + "time": "2017-09-30T12:15:07+00:00" }, { "name": "macfja/symfony-console-filechooser", - "version": "0.1", + "version": "0.2", "source": { "type": "git", "url": "https://github.com/MacFJA/Symfony-Console-Filechooser.git", - "reference": "9d9db462a02d3dba1b1499e9f88d06a3aea40e2a" + "reference": "abed2593d4a75130b4472cb9de62936a8498e096" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/MacFJA/Symfony-Console-Filechooser/zipball/9d9db462a02d3dba1b1499e9f88d06a3aea40e2a", - "reference": "9d9db462a02d3dba1b1499e9f88d06a3aea40e2a", + "url": "https://api.github.com/repos/MacFJA/Symfony-Console-Filechooser/zipball/abed2593d4a75130b4472cb9de62936a8498e096", + "reference": "abed2593d4a75130b4472cb9de62936a8498e096", "shasum": "" }, "require": { - "symfony/console": "~2.6", - "symfony/finder": "~2.6" + "symfony/console": "~2.6 || ^3.0", + "symfony/finder": "~2.6 || ^3.0" }, "type": "library", "autoload": { @@ -1420,41 +1917,44 @@ "interactive", "symfony" ], - "time": "2015-09-23T18:04:52+00:00" + "time": "2017-08-05T19:16:50+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.6.1", + "version": "1.7.0", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", - "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", + "reference": "3b8a3a99ba1f6a3952ac2747d989303cbd6b7a3e", "shasum": "" }, "require": { - "php": ">=5.4.0" + "php": "^5.6 || ^7.0" }, "require-dev": { - "doctrine/collections": "1.*", - "phpunit/phpunit": "~4.1" + "doctrine/collections": "^1.0", + "doctrine/common": "^2.6", + "phpunit/phpunit": "^4.1" }, "type": "library", "autoload": { "psr-4": { "DeepCopy\\": "src/DeepCopy/" - } + }, + "files": [ + "src/DeepCopy/deep_copy.php" + ] }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "description": "Create deep copies (clones) of your objects", - "homepage": "https://github.com/myclabs/DeepCopy", "keywords": [ "clone", "copy", @@ -1462,7 +1962,7 @@ "object", "object graph" ], - "time": "2017-04-12T18:52:22+00:00" + "time": "2017-10-19T19:58:43+00:00" }, { "name": "neutron/signal-handler", @@ -1508,17 +2008,215 @@ "time": "2014-01-15T17:24:13+00:00" }, { - "name": "phpdocumentor/reflection-common", - "version": "1.0", + "name": "paragonie/random_compat", + "version": "v2.0.11", "source": { "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionCommon.git", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" + "url": "https://github.com/paragonie/random_compat.git", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", - "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", + "url": "https://api.github.com/repos/paragonie/random_compat/zipball/5da4d3c796c275c55f057af5a643ae297d96b4d8", + "reference": "5da4d3c796c275c55f057af5a643ae297d96b4d8", + "shasum": "" + }, + "require": { + "php": ">=5.2.0" + }, + "require-dev": { + "phpunit/phpunit": "4.*|5.*" + }, + "suggest": { + "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." + }, + "type": "library", + "autoload": { + "files": [ + "lib/random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paragon Initiative Enterprises", + "email": "security@paragonie.com", + "homepage": "https://paragonie.com" + } + ], + "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", + "keywords": [ + "csprng", + "pseudorandom", + "random" + ], + "time": "2017-09-27T21:40:39+00:00" + }, + { + "name": "phar-io/manifest", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/2df402786ab5368a0169091f61a7c1e0eb6852d0", + "reference": "2df402786ab5368a0169091f61a7c1e0eb6852d0", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-phar": "*", + "phar-io/version": "^1.0.1", + "php": "^5.6 || ^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "time": "2017-03-05T18:14:27+00:00" + }, + { + "name": "phar-io/version", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/a70c0ced4be299a63d32fa96d9281d03e94041df", + "reference": "a70c0ced4be299a63d32fa96d9281d03e94041df", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "time": "2017-03-05T17:38:23+00:00" + }, + { + "name": "php-cs-fixer/diff", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/PHP-CS-Fixer/diff.git", + "reference": "f0ef6133d674137e902fdf8a6f2e8e97e14a087b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/f0ef6133d674137e902fdf8a6f2e8e97e14a087b", + "reference": "f0ef6133d674137e902fdf8a6f2e8e97e14a087b", + "shasum": "" + }, + "require": { + "php": "^5.6 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.4.3", + "symfony/process": "^3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "SpacePossum" + } + ], + "description": "sebastian/diff v2 backport support for PHP5.6", + "homepage": "https://github.com/PHP-CS-Fixer", + "keywords": [ + "diff" + ], + "time": "2017-10-19T09:58:18+00:00" + }, + { + "name": "phpdocumentor/reflection-common", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", + "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", "shasum": "" }, "require": { @@ -1559,33 +2257,39 @@ "reflection", "static analysis" ], - "time": "2015-12-27T11:43:31+00:00" + "time": "2017-09-11T18:02:19+00:00" }, { "name": "phpdocumentor/reflection-docblock", - "version": "3.1.1", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" + "reference": "66465776cfc249844bde6d117abff1d22e06c2da" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", - "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/66465776cfc249844bde6d117abff1d22e06c2da", + "reference": "66465776cfc249844bde6d117abff1d22e06c2da", "shasum": "" }, "require": { - "php": ">=5.5", - "phpdocumentor/reflection-common": "^1.0@dev", - "phpdocumentor/type-resolver": "^0.2.0", + "php": "^7.0", + "phpdocumentor/reflection-common": "^1.0.0", + "phpdocumentor/type-resolver": "^0.4.0", "webmozart/assert": "^1.0" }, "require-dev": { - "mockery/mockery": "^0.9.4", - "phpunit/phpunit": "^4.4" + "doctrine/instantiator": "~1.0.5", + "mockery/mockery": "^1.0", + "phpunit/phpunit": "^6.4" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.x-dev" + } + }, "autoload": { "psr-4": { "phpDocumentor\\Reflection\\": [ @@ -1604,24 +2308,24 @@ } ], "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", - "time": "2016-09-30T07:12:33+00:00" + "time": "2017-11-27T17:38:31+00:00" }, { "name": "phpdocumentor/type-resolver", - "version": "0.2.1", + "version": "0.4.0", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", - "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", + "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", "shasum": "" }, "require": { - "php": ">=5.5", + "php": "^5.5 || ^7.0", "phpdocumentor/reflection-common": "^1.0" }, "require-dev": { @@ -1651,37 +2355,37 @@ "email": "me@mikevanriel.com" } ], - "time": "2016-11-25T06:54:22+00:00" + "time": "2017-07-14T14:27:02+00:00" }, { "name": "phpspec/prophecy", - "version": "v1.7.0", + "version": "1.7.3", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", - "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", + "reference": "e4ed002c67da8eceb0eb8ddb8b3847bb53c5c2bf", "shasum": "" }, "require": { "doctrine/instantiator": "^1.0.2", "php": "^5.3|^7.0", - "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0", "sebastian/comparator": "^1.1|^2.0", "sebastian/recursion-context": "^1.0|^2.0|^3.0" }, "require-dev": { "phpspec/phpspec": "^2.5|^3.2", - "phpunit/phpunit": "^4.8 || ^5.6.5" + "phpunit/phpunit": "^4.8.35 || ^5.7" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.6.x-dev" + "dev-master": "1.7.x-dev" } }, "autoload": { @@ -1714,44 +2418,44 @@ "spy", "stub" ], - "time": "2017-03-02T20:05:34+00:00" + "time": "2017-11-24T13:59:53+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "4.0.8", + "version": "5.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", - "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/661f34d0bd3f1a7225ef491a70a020ad23a057a1", + "reference": "661f34d0bd3f1a7225ef491a70a020ad23a057a1", "shasum": "" }, "require": { "ext-dom": "*", "ext-xmlwriter": "*", - "php": "^5.6 || ^7.0", - "phpunit/php-file-iterator": "^1.3", - "phpunit/php-text-template": "^1.2", - "phpunit/php-token-stream": "^1.4.2 || ^2.0", - "sebastian/code-unit-reverse-lookup": "^1.0", - "sebastian/environment": "^1.3.2 || ^2.0", - "sebastian/version": "^1.0 || ^2.0" + "php": "^7.0", + "phpunit/php-file-iterator": "^1.4.2", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-token-stream": "^2.0.1", + "sebastian/code-unit-reverse-lookup": "^1.0.1", + "sebastian/environment": "^3.0", + "sebastian/version": "^2.0.1", + "theseer/tokenizer": "^1.1" }, "require-dev": { - "ext-xdebug": "^2.1.4", - "phpunit/phpunit": "^5.7" + "phpunit/phpunit": "^6.0" }, "suggest": { - "ext-xdebug": "^2.5.1" + "ext-xdebug": "^2.5.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0.x-dev" + "dev-master": "5.3.x-dev" } }, "autoload": { @@ -1766,7 +2470,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -1777,20 +2481,20 @@ "testing", "xunit" ], - "time": "2017-04-02T07:44:40+00:00" + "time": "2017-12-06T09:29:45+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "1.4.2", + "version": "1.4.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", - "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", + "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", "shasum": "" }, "require": { @@ -1824,7 +2528,7 @@ "filesystem", "iterator" ], - "time": "2016-10-03T07:40:28+00:00" + "time": "2017-11-27T13:52:08+00:00" }, { "name": "phpunit/php-text-template", @@ -1918,29 +2622,29 @@ }, { "name": "phpunit/php-token-stream", - "version": "1.4.11", + "version": "2.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" + "reference": "791198a2c6254db10131eecfe8c06670700904db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", - "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/791198a2c6254db10131eecfe8c06670700904db", + "reference": "791198a2c6254db10131eecfe8c06670700904db", "shasum": "" }, "require": { "ext-tokenizer": "*", - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -1963,20 +2667,20 @@ "keywords": [ "tokenizer" ], - "time": "2017-02-27T10:12:30+00:00" + "time": "2017-11-27T05:48:46+00:00" }, { "name": "phpunit/phpunit", - "version": "5.7.19", + "version": "6.5.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" + "reference": "83d27937a310f2984fd575686138597147bdc7df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/83d27937a310f2984fd575686138597147bdc7df", + "reference": "83d27937a310f2984fd575686138597147bdc7df", "shasum": "" }, "require": { @@ -1985,33 +2689,35 @@ "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", - "myclabs/deep-copy": "~1.3", - "php": "^5.6 || ^7.0", - "phpspec/prophecy": "^1.6.2", - "phpunit/php-code-coverage": "^4.0.4", - "phpunit/php-file-iterator": "~1.4", - "phpunit/php-text-template": "~1.2", - "phpunit/php-timer": "^1.0.6", - "phpunit/phpunit-mock-objects": "^3.2", - "sebastian/comparator": "^1.2.4", - "sebastian/diff": "~1.2", - "sebastian/environment": "^1.3.4 || ^2.0", - "sebastian/exporter": "~2.0", - "sebastian/global-state": "^1.1", - "sebastian/object-enumerator": "~2.0", - "sebastian/resource-operations": "~1.0", - "sebastian/version": "~1.0.3|~2.0", - "symfony/yaml": "~2.1|~3.0" + "myclabs/deep-copy": "^1.6.1", + "phar-io/manifest": "^1.0.1", + "phar-io/version": "^1.0", + "php": "^7.0", + "phpspec/prophecy": "^1.7", + "phpunit/php-code-coverage": "^5.3", + "phpunit/php-file-iterator": "^1.4.3", + "phpunit/php-text-template": "^1.2.1", + "phpunit/php-timer": "^1.0.9", + "phpunit/phpunit-mock-objects": "^5.0.5", + "sebastian/comparator": "^2.1", + "sebastian/diff": "^2.0", + "sebastian/environment": "^3.1", + "sebastian/exporter": "^3.1", + "sebastian/global-state": "^2.0", + "sebastian/object-enumerator": "^3.0.3", + "sebastian/resource-operations": "^1.0", + "sebastian/version": "^2.0.1" }, "conflict": { - "phpdocumentor/reflection-docblock": "3.0.2" + "phpdocumentor/reflection-docblock": "3.0.2", + "phpunit/dbunit": "<3.0" }, "require-dev": { "ext-pdo": "*" }, "suggest": { "ext-xdebug": "*", - "phpunit/php-invoker": "~1.1" + "phpunit/php-invoker": "^1.1" }, "bin": [ "phpunit" @@ -2019,7 +2725,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.7.x-dev" + "dev-master": "6.5.x-dev" } }, "autoload": { @@ -2045,33 +2751,33 @@ "testing", "xunit" ], - "time": "2017-04-03T02:22:27+00:00" + "time": "2017-12-17T06:31:19+00:00" }, { "name": "phpunit/phpunit-mock-objects", - "version": "3.4.3", + "version": "5.0.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" + "reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", - "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/283b9f4f670e3a6fd6c4ff95c51a952eb5c75933", + "reference": "283b9f4f670e3a6fd6c4ff95c51a952eb5c75933", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.0.2", - "php": "^5.6 || ^7.0", - "phpunit/php-text-template": "^1.2", - "sebastian/exporter": "^1.2 || ^2.0" + "doctrine/instantiator": "^1.0.5", + "php": "^7.0", + "phpunit/php-text-template": "^1.2.1", + "sebastian/exporter": "^3.1" }, "conflict": { - "phpunit/phpunit": "<5.4.0" + "phpunit/phpunit": "<6.0" }, "require-dev": { - "phpunit/phpunit": "^5.4" + "phpunit/phpunit": "^6.5" }, "suggest": { "ext-soap": "*" @@ -2079,7 +2785,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2.x-dev" + "dev-master": "5.0.x-dev" } }, "autoload": { @@ -2094,7 +2800,7 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", + "email": "sebastian@phpunit.de", "role": "lead" } ], @@ -2104,7 +2810,7 @@ "mock", "xunit" ], - "time": "2016-12-08T20:27:08+00:00" + "time": "2017-12-10T08:01:53+00:00" }, { "name": "psr/log", @@ -2245,30 +2951,30 @@ }, { "name": "sebastian/comparator", - "version": "1.2.4", + "version": "2.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + "reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", - "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b11c729f95109b56a0fe9650c6a63a0fcd8c439f", + "reference": "b11c729f95109b56a0fe9650c6a63a0fcd8c439f", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/diff": "~1.2", - "sebastian/exporter": "~1.2 || ~2.0" + "php": "^7.0", + "sebastian/diff": "^2.0", + "sebastian/exporter": "^3.1" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.4" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.2.x-dev" + "dev-master": "2.1.x-dev" } }, "autoload": { @@ -2299,38 +3005,38 @@ } ], "description": "Provides the functionality to compare PHP values for equality", - "homepage": "http://www.github.com/sebastianbergmann/comparator", + "homepage": "https://github.com/sebastianbergmann/comparator", "keywords": [ "comparator", "compare", "equality" ], - "time": "2017-01-29T09:50:25+00:00" + "time": "2017-12-22T14:50:35+00:00" }, { "name": "sebastian/diff", - "version": "1.4.1", + "version": "2.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", + "reference": "347c1d8b49c5c3ee30c7040ea6fc446790e6bddd", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "^6.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.4-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2357,32 +3063,32 @@ "keywords": [ "diff" ], - "time": "2015-12-08T07:14:41+00:00" + "time": "2017-08-03T08:09:46+00:00" }, { "name": "sebastian/environment", - "version": "2.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", - "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/cd0871b3975fb7fc44d11314fd1ee20925fce4f5", + "reference": "cd0871b3975fb7fc44d11314fd1ee20925fce4f5", "shasum": "" }, "require": { - "php": "^5.6 || ^7.0" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "^5.0" + "phpunit/phpunit": "^6.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -2407,34 +3113,34 @@ "environment", "hhvm" ], - "time": "2016-11-26T07:53:53+00:00" + "time": "2017-07-01T08:51:00+00:00" }, { "name": "sebastian/exporter", - "version": "2.0.0", + "version": "3.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", - "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/234199f4528de6d12aaa58b612e98f7d36adb937", + "reference": "234199f4528de6d12aaa58b612e98f7d36adb937", "shasum": "" }, "require": { - "php": ">=5.3.3", - "sebastian/recursion-context": "~2.0" + "php": "^7.0", + "sebastian/recursion-context": "^3.0" }, "require-dev": { "ext-mbstring": "*", - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.1.x-dev" } }, "autoload": { @@ -2474,27 +3180,27 @@ "export", "exporter" ], - "time": "2016-11-19T08:54:04+00:00" + "time": "2017-04-03T13:19:02+00:00" }, { "name": "sebastian/global-state", - "version": "1.1.1", + "version": "2.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", - "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", + "reference": "e8ba02eed7bbbb9e59e43dedd3dddeff4a56b0c4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.2" + "phpunit/phpunit": "^6.0" }, "suggest": { "ext-uopz": "*" @@ -2502,7 +3208,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-master": "2.0-dev" } }, "autoload": { @@ -2525,33 +3231,34 @@ "keywords": [ "global state" ], - "time": "2015-10-12T03:26:01+00:00" + "time": "2017-04-27T15:39:26+00:00" }, { "name": "sebastian/object-enumerator", - "version": "2.0.1", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", - "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/7cfd9e65d11ffb5af41198476395774d4c8a84c5", + "reference": "7cfd9e65d11ffb5af41198476395774d4c8a84c5", "shasum": "" }, "require": { - "php": ">=5.6", - "sebastian/recursion-context": "~2.0" + "php": "^7.0", + "sebastian/object-reflector": "^1.1.1", + "sebastian/recursion-context": "^3.0" }, "require-dev": { - "phpunit/phpunit": "~5" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -2571,32 +3278,77 @@ ], "description": "Traverses array structures and object graphs to enumerate all referenced objects", "homepage": "https://github.com/sebastianbergmann/object-enumerator/", - "time": "2017-02-18T15:18:39+00:00" + "time": "2017-08-03T12:35:26+00:00" }, { - "name": "sebastian/recursion-context", - "version": "2.0.0", + "name": "sebastian/object-reflector", + "version": "1.1.1", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "773f97c67f28de00d397be301821b06708fca0be" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", - "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/773f97c67f28de00d397be301821b06708fca0be", + "reference": "773f97c67f28de00d397be301821b06708fca0be", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.4" + "phpunit/phpunit": "^6.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0.x-dev" + "dev-master": "1.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "time": "2017-03-29T09:07:27+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "reference": "5b0cd723502bac3b006cbf3dbf7a1e3fcefe4fa8", + "shasum": "" + }, + "require": { + "php": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^6.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" } }, "autoload": { @@ -2624,7 +3376,7 @@ ], "description": "Provides functionality to recursively process PHP variables", "homepage": "http://www.github.com/sebastianbergmann/recursion-context", - "time": "2016-11-19T07:33:16+00:00" + "time": "2017-03-03T06:23:57+00:00" }, { "name": "sebastian/resource-operations", @@ -2713,37 +3465,45 @@ }, { "name": "symfony/console", - "version": "v2.8.20", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e" + "reference": "9f21adfb92a9315b73ae2ed43138988ee4913d4e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e", - "reference": "2cfcbced8e39e2313ed4da8896fc8c59a56c0d7e", + "url": "https://api.github.com/repos/symfony/console/zipball/9f21adfb92a9315b73ae2ed43138988ee4913d4e", + "reference": "9f21adfb92a9315b73ae2ed43138988ee4913d4e", "shasum": "" }, "require": { - "php": ">=5.3.9", - "symfony/debug": "^2.7.2|~3.0.0", + "php": "^5.5.9|>=7.0.8", + "symfony/debug": "~2.8|~3.0|~4.0", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "symfony/dependency-injection": "<3.4", + "symfony/process": "<3.3" + }, "require-dev": { "psr/log": "~1.0", - "symfony/event-dispatcher": "~2.1|~3.0.0", - "symfony/process": "~2.1|~3.0.0" + "symfony/config": "~3.3|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/event-dispatcher": "~2.8|~3.0|~4.0", + "symfony/lock": "~3.4|~4.0", + "symfony/process": "~3.3|~4.0" }, "suggest": { "psr/log": "For using the console logger", "symfony/event-dispatcher": "", + "symfony/lock": "", "symfony/process": "" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -2770,37 +3530,36 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-04-26T01:38:53+00:00" + "time": "2017-12-14T19:40:10+00:00" }, { "name": "symfony/debug", - "version": "v3.0.9", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a" + "reference": "8c3e709209ce3b952a31c0f4a31ac7703c3d0226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/697c527acd9ea1b2d3efac34d9806bf255278b0a", - "reference": "697c527acd9ea1b2d3efac34d9806bf255278b0a", + "url": "https://api.github.com/repos/symfony/debug/zipball/8c3e709209ce3b952a31c0f4a31ac7703c3d0226", + "reference": "8c3e709209ce3b952a31c0f4a31ac7703c3d0226", "shasum": "" }, "require": { - "php": ">=5.5.9", + "php": "^7.1.3", "psr/log": "~1.0" }, "conflict": { - "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" + "symfony/http-kernel": "<3.4" }, "require-dev": { - "symfony/class-loader": "~2.8|~3.0", - "symfony/http-kernel": "~2.8|~3.0" + "symfony/http-kernel": "~3.4|~4.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2827,31 +3586,34 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2016-07-30T07:22:48+00:00" + "time": "2017-12-12T08:41:51+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v3.2.8", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b8a401f733b43251e1d088c589368b2a94155e40" + "reference": "d4face19ed8002eec8280bc1c5ec18130472bf43" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b8a401f733b43251e1d088c589368b2a94155e40", - "reference": "b8a401f733b43251e1d088c589368b2a94155e40", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d4face19ed8002eec8280bc1c5ec18130472bf43", + "reference": "d4face19ed8002eec8280bc1c5ec18130472bf43", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^7.1.3" + }, + "conflict": { + "symfony/dependency-injection": "<3.4" }, "require-dev": { "psr/log": "~1.0", - "symfony/config": "~2.8|~3.0", - "symfony/dependency-injection": "~2.8|~3.0", - "symfony/expression-language": "~2.8|~3.0", - "symfony/stopwatch": "~2.8|~3.0" + "symfony/config": "~3.4|~4.0", + "symfony/dependency-injection": "~3.4|~4.0", + "symfony/expression-language": "~3.4|~4.0", + "symfony/stopwatch": "~3.4|~4.0" }, "suggest": { "symfony/dependency-injection": "", @@ -2860,7 +3622,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2887,29 +3649,29 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "https://symfony.com", - "time": "2017-05-01T14:58:48+00:00" + "time": "2017-12-14T19:48:22+00:00" }, { "name": "symfony/filesystem", - "version": "v3.2.8", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "040651db13cf061827a460cc10f6e36a445c45b4" + "reference": "8c2868641d0c4885eee9c12a89c2b695eb1985cd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/040651db13cf061827a460cc10f6e36a445c45b4", - "reference": "040651db13cf061827a460cc10f6e36a445c45b4", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/8c2868641d0c4885eee9c12a89c2b695eb1985cd", + "reference": "8c2868641d0c4885eee9c12a89c2b695eb1985cd", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^7.1.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -2936,29 +3698,29 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-12-14T19:48:22+00:00" }, { "name": "symfony/finder", - "version": "v2.8.20", + "version": "v3.4.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "16d55394b31547e4a8494551b85c9b9915545347" + "reference": "dac8d7db537bac7ad8143eb11360a8c2231f251a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/16d55394b31547e4a8494551b85c9b9915545347", - "reference": "16d55394b31547e4a8494551b85c9b9915545347", + "url": "https://api.github.com/repos/symfony/finder/zipball/dac8d7db537bac7ad8143eb11360a8c2231f251a", + "reference": "dac8d7db537bac7ad8143eb11360a8c2231f251a", "shasum": "" }, "require": { - "php": ">=5.3.9" + "php": "^5.5.9|>=7.0.8" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.8-dev" + "dev-master": "3.4-dev" } }, "autoload": { @@ -2985,20 +3747,74 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:07:15+00:00" + "time": "2017-11-05T16:10:10+00:00" }, { - "name": "symfony/polyfill-mbstring", - "version": "v1.3.0", + "name": "symfony/options-resolver", + "version": "v4.0.2", "source": { "type": "git", - "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4" + "url": "https://github.com/symfony/options-resolver.git", + "reference": "75fdda335eb0adbd464089e8a0184c61097808e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/e79d363049d1c2128f133a2667e4f4190904f7f4", - "reference": "e79d363049d1c2128f133a2667e4f4190904f7f4", + "url": "https://api.github.com/repos/symfony/options-resolver/zipball/75fdda335eb0adbd464089e8a0184c61097808e0", + "reference": "75fdda335eb0adbd464089e8a0184c61097808e0", + "shasum": "" + }, + "require": { + "php": "^7.1.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\OptionsResolver\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony OptionsResolver Component", + "homepage": "https://symfony.com", + "keywords": [ + "config", + "configuration", + "options" + ], + "time": "2017-12-14T19:48:22+00:00" + }, + { + "name": "symfony/polyfill-mbstring", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-mbstring.git", + "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", + "reference": "2ec8b39c38cb16674bbf3fea2b6ce5bf117e1296", "shasum": "" }, "require": { @@ -3010,7 +3826,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.3-dev" + "dev-master": "1.6-dev" } }, "autoload": { @@ -3044,29 +3860,143 @@ "portable", "shim" ], - "time": "2016-11-14T01:06:16+00:00" + "time": "2017-10-11T12:05:26+00:00" }, { - "name": "symfony/process", - "version": "v3.2.8", + "name": "symfony/polyfill-php70", + "version": "v1.6.0", "source": { "type": "git", - "url": "https://github.com/symfony/process.git", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0" + "url": "https://github.com/symfony/polyfill-php70.git", + "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff", + "reference": "0442b9c0596610bd24ae7b5f0a6cdbbc16d9fcff", "shasum": "" }, "require": { - "php": ">=5.5.9" + "paragonie/random_compat": "~1.0|~2.0", + "php": ">=5.3.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php70\\": "" + }, + "files": [ + "bootstrap.php" + ], + "classmap": [ + "Resources/stubs" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-10-11T12:05:26+00:00" + }, + { + "name": "symfony/polyfill-php72", + "version": "v1.6.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/polyfill-php72.git", + "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/6de4f4884b97abbbed9f0a84a95ff2ff77254254", + "reference": "6de4f4884b97abbbed9f0a84a95ff2ff77254254", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Polyfill\\Php72\\": "" + }, + "files": [ + "bootstrap.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", + "homepage": "https://symfony.com", + "keywords": [ + "compatibility", + "polyfill", + "portable", + "shim" + ], + "time": "2017-10-11T12:05:26+00:00" + }, + { + "name": "symfony/process", + "version": "v3.4.2", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "bb3ef65d493a6d57297cad6c560ee04e2a8f5098" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/bb3ef65d493a6d57297cad6c560ee04e2a8f5098", + "reference": "bb3ef65d493a6d57297cad6c560ee04e2a8f5098", + "shasum": "" + }, + "require": { + "php": "^5.5.9|>=7.0.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.4-dev" } }, "autoload": { @@ -3093,29 +4023,29 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-12-14T19:40:10+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.2.8", + "version": "v4.0.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a0105afb670dbd38f521105c444de1b8e10cfe3" + "reference": "ac0e49150555c703fef6b696d8eaba1db7a3ca03" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a0105afb670dbd38f521105c444de1b8e10cfe3", - "reference": "5a0105afb670dbd38f521105c444de1b8e10cfe3", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/ac0e49150555c703fef6b696d8eaba1db7a3ca03", + "reference": "ac0e49150555c703fef6b696d8eaba1db7a3ca03", "shasum": "" }, "require": { - "php": ">=5.5.9" + "php": "^7.1.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "4.0-dev" } }, "autoload": { @@ -3142,7 +4072,47 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-11-09T12:45:29+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.1.0", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "reference": "cb2f008f3f05af2893a87208fe6a6c4985483f8b", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "time": "2017-04-07T12:08:54+00:00" }, { "name": "webignition/readable-duration", @@ -3189,12 +4159,12 @@ } ], "aliases": [], - "minimum-stability": "dev", + "minimum-stability": "stable", "stability-flags": [], - "prefer-stable": true, + "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^5.5|^7", + "php": ">=7", "ext-openssl": "*" }, "platform-dev": [] diff --git a/doc/migrations/0.3.0.md b/doc/migrations/0.3.0.md new file mode 100644 index 0000000..142ec12 --- /dev/null +++ b/doc/migrations/0.3.0.md @@ -0,0 +1,5 @@ +# Migration from 0.2.x to 0.3.x + +If you used this client before `0.3.0` via the command line, nothing should change with this release. It is an internal rewrite to Amp v2, which is the underlying concurrency framework. + +If you're depending on this package's internals, some things might have changed slightly. A detailed changelog can't be provided. My focus is the public command line API. \ No newline at end of file diff --git a/src/AcmeFactory.php b/src/AcmeFactory.php index c037ada..427e5a1 100644 --- a/src/AcmeFactory.php +++ b/src/AcmeFactory.php @@ -4,13 +4,10 @@ namespace Kelunik\AcmeClient; use Kelunik\Acme\AcmeClient; use Kelunik\Acme\AcmeService; -use Kelunik\Acme\KeyPair; -use Webmozart\Assert\Assert; +use Kelunik\Acme\Crypto\PrivateKey; class AcmeFactory { - public function build($directory, KeyPair $keyPair) { - Assert::string($directory); - + public function build(string $directory, PrivateKey $keyPair): AcmeService { return new AcmeService(new AcmeClient($directory, $keyPair)); } } \ No newline at end of file diff --git a/src/Commands/Auto.php b/src/Commands/Auto.php index ca6a9a8..13caf94 100644 --- a/src/Commands/Auto.php +++ b/src/Commands/Auto.php @@ -2,15 +2,19 @@ namespace Kelunik\AcmeClient\Commands; -use Amp\CoroutineResult; +use Amp\ByteStream\Message; +use Amp\File; use Amp\File\FilesystemException; -use Amp\Process; +use Amp\Process\Process; +use Amp\Promise; use Kelunik\Acme\AcmeException; +use Kelunik\AcmeClient; use Kelunik\AcmeClient\ConfigException; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; +use function Amp\call; class Auto implements Command { const EXIT_CONFIG_ERROR = 1; @@ -28,230 +32,221 @@ class Auto implements Command { $this->climate = $climate; } - public function execute(Manager $args) { - return \Amp\resolve($this->doExecute($args)); - } + public function execute(Manager $args): Promise { + return call(function () use ($args) { + $configPath = $args->get('config'); - /** - * @param Manager $args - * @return \Generator - */ - private function doExecute(Manager $args) { - $configPath = $args->get("config"); - - try { - $config = Yaml::parse( - yield \Amp\File\get($configPath) - ); - } catch (FilesystemException $e) { - $this->climate->error("Config file ({$configPath}) not found."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } catch (ParseException $e) { - $this->climate->error("Config file ({$configPath}) had an invalid format and couldn't be parsed."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } - - if ($args->defined("server")) { - $config["server"] = $args->get("server"); - } else if (!isset($config["server"]) && $args->exists("server")) { - $config["server"] = $args->get("server"); - } - - if ($args->defined("storage")) { - $config["storage"] = $args->get("storage"); - } else if (!isset($config["storage"]) && $args->exists("storage")) { - $config["storage"] = $args->get("storage"); - } - - if (!isset($config["server"])) { - $this->climate->error("Config file ({$configPath}) didn't have a 'server' set nor was it passed as command line argument."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } - - if (!isset($config["storage"])) { - $this->climate->error("Config file ({$configPath}) didn't have a 'storage' set nor was it passed as command line argument."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } - - if (!isset($config["email"])) { - $this->climate->error("Config file ({$configPath}) didn't have a 'email' set."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } - - if (!isset($config["certificates"]) || !is_array($config["certificates"])) { - $this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section that's an array."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } - - if (isset($config["challenge-concurrency"]) && !is_numeric($config["challenge-concurrency"])) { - $this->climate->error("Config file ({$configPath}) defines an invalid 'challenge-concurrency' value."); - yield new CoroutineResult(self::EXIT_CONFIG_ERROR); - return; - } - - $concurrency = isset($config["challenge-concurrency"]) ? (int) $config["challenge-concurrency"] : null; - - $command = implode(" ", array_map("escapeshellarg", [ - PHP_BINARY, - $GLOBALS["argv"][0], - "setup", - "--server", - $config["server"], - "--storage", - $config["storage"], - "--email", - $config["email"], - ])); - - $process = new Process($command); - $result = (yield $process->exec(Process::BUFFER_ALL)); - - if ($result->exit !== 0) { - $this->climate->error("Registration failed ({$result->exit})"); - $this->climate->error($command); - $this->climate->br()->out($result->stdout); - $this->climate->br()->error($result->stderr); - yield new CoroutineResult(self::EXIT_SETUP_ERROR); - return; - } - - $errors = []; - $values = []; - - foreach ($config["certificates"] as $i => $certificate) { try { - $result = (yield \Amp\resolve($this->checkAndIssue($certificate, $config["server"], $config["storage"], $concurrency))); - $values[$i] = $result; - } catch (\Exception $e) { - $errors[$i] = $e; + /** @var array $config */ + $config = Yaml::parse( + yield File\get($configPath) + ); + } catch (FilesystemException $e) { + $this->climate->error("Config file ({$configPath}) not found."); + return self::EXIT_CONFIG_ERROR; + } catch (ParseException $e) { + $this->climate->error("Config file ({$configPath}) had an invalid format and couldn't be parsed."); + return self::EXIT_CONFIG_ERROR; } - } - $status = [ - "no_change" => count(array_filter($values, function($value) { return $value === self::STATUS_NO_CHANGE; })), - "renewed" => count(array_filter($values, function($value) { return $value === self::STATUS_RENEWED; })), - "failure" => count($errors), - ]; + if ($args->defined('server')) { + $config['server'] = $args->get('server'); + } else if (!isset($config['server']) && $args->exists('server')) { + $config['server'] = $args->get('server'); + } - if ($status["renewed"] > 0) { - foreach ($values as $i => $value) { - if ($value === self::STATUS_RENEWED) { - $certificate = $config["certificates"][$i]; - $this->climate->info("Certificate for " . implode(", ", array_keys($this->toDomainPathMap($certificate["paths"]))) . " successfully renewed."); + if ($args->defined('storage')) { + $config['storage'] = $args->get('storage'); + } else if (!isset($config['storage']) && $args->exists('storage')) { + $config['storage'] = $args->get('storage'); + } + + if (!isset($config['server'])) { + $this->climate->error("Config file ({$configPath}) didn't have a 'server' set nor was it passed as command line argument."); + return self::EXIT_CONFIG_ERROR; + } + + if (!isset($config['storage'])) { + $this->climate->error("Config file ({$configPath}) didn't have a 'storage' set nor was it passed as command line argument."); + return self::EXIT_CONFIG_ERROR; + } + + if (!isset($config['email'])) { + $this->climate->error("Config file ({$configPath}) didn't have a 'email' set."); + return self::EXIT_CONFIG_ERROR; + } + + if (!isset($config['certificates']) || !\is_array($config['certificates'])) { + $this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section that's an array."); + return self::EXIT_CONFIG_ERROR; + } + + if (isset($config['challenge-concurrency']) && !is_numeric($config['challenge-concurrency'])) { + $this->climate->error("Config file ({$configPath}) defines an invalid 'challenge-concurrency' value."); + return self::EXIT_CONFIG_ERROR; + } + + $concurrency = isset($config['challenge-concurrency']) ? (int) $config['challenge-concurrency'] : null; + + $process = new Process([ + PHP_BINARY, + $GLOBALS['argv'][0], + 'setup', + '--server', + $config['server'], + '--storage', + $config['storage'], + '--email', + $config['email'], + ]); + + $process->start(); + $exit = yield $process->join(); + + if ($exit !== 0) { + $this->climate->error("Registration failed ({$exit})"); + $this->climate->br()->out(yield new Message($process->getStdout())); + $this->climate->br()->error(yield new Message($process->getStderr())); + + return self::EXIT_SETUP_ERROR; + } + + $errors = []; + $values = []; + + foreach ($config['certificates'] as $i => $certificate) { + try { + $exit = yield call(function () use ($certificate, $config, $concurrency) { + return $this->checkAndIssue($certificate, $config['server'], $config['storage'], $concurrency); + }); + + $values[$i] = $exit; + } catch (\Exception $e) { + $errors[$i] = $e; } } - } - if ($status["failure"] > 0) { - foreach ($errors as $i => $error) { - $certificate = $config["certificates"][$i]; - $this->climate->error("Issuance for the following domains failed: " . implode(", ", array_keys($this->toDomainPathMap($certificate["paths"])))); - $this->climate->error("Reason: {$error}"); + $status = [ + 'no_change' => \count(\array_filter($values, function ($value) { + return $value === self::STATUS_NO_CHANGE; + })), + 'renewed' => \count(\array_filter($values, function ($value) { + return $value === self::STATUS_RENEWED; + })), + 'failure' => \count($errors), + ]; + + if ($status['renewed'] > 0) { + foreach ($values as $i => $value) { + if ($value === self::STATUS_RENEWED) { + $certificate = $config['certificates'][$i]; + $this->climate->info('Certificate for ' . implode(', ', array_keys($this->toDomainPathMap($certificate['paths']))) . ' successfully renewed.'); + } + } } - $exitCode = $status["renewed"] > 0 - ? self::EXIT_ISSUANCE_PARTIAL - : self::EXIT_ISSUANCE_ERROR; + if ($status['failure'] > 0) { + foreach ($errors as $i => $error) { + $certificate = $config['certificates'][$i]; + $this->climate->error('Issuance for the following domains failed: ' . implode(', ', array_keys($this->toDomainPathMap($certificate['paths'])))); + $this->climate->error("Reason: {$error}"); + } - yield new CoroutineResult($exitCode); - return; - } + $exitCode = $status['renewed'] > 0 + ? self::EXIT_ISSUANCE_PARTIAL + : self::EXIT_ISSUANCE_ERROR; - if ($status["renewed"] > 0) { - yield new CoroutineResult(self::EXIT_ISSUANCE_OK); - return; - } + return $exitCode; + } + + if ($status['renewed'] > 0) { + return self::EXIT_ISSUANCE_OK; + } + }); } /** - * @param array $certificate certificate configuration - * @param string $server server to use for issuance - * @param string $storage storage directory + * @param array $certificate certificate configuration + * @param string $server server to use for issuance + * @param string $storage storage directory * @param int|null $concurrency concurrent challenges + * * @return \Generator * @throws AcmeException if something does wrong + * @throws \Throwable */ - private function checkAndIssue(array $certificate, $server, $storage, $concurrency = null) { - $domainPathMap = $this->toDomainPathMap($certificate["paths"]); + private function checkAndIssue(array $certificate, string $server, string $storage, int $concurrency = null): \Generator { + $domainPathMap = $this->toDomainPathMap($certificate['paths']); $domains = array_keys($domainPathMap); $commonName = reset($domains); - $args = [ + $process = new Process([ PHP_BINARY, - $GLOBALS["argv"][0], - "check", - "--server", + $GLOBALS['argv'][0], + 'check', + '--server', $server, - "--storage", + '--storage', $storage, - "--name", + '--name', $commonName, - "--names", - implode(",", $domains), - ]; + '--names', + implode(',', $domains), + ]); - $command = implode(" ", array_map("escapeshellarg", $args)); + $process->start(); + $exit = yield $process->join(); - $process = new Process($command); - $result = (yield $process->exec(Process::BUFFER_ALL)); - - if ($result->exit === 0) { + if ($exit === 0) { // No need for renewal - yield new CoroutineResult(self::STATUS_NO_CHANGE); - return; + return self::STATUS_NO_CHANGE; } - if ($result->exit === 1) { + if ($exit === 1) { // Renew certificate $args = [ PHP_BINARY, - $GLOBALS["argv"][0], - "issue", - "--server", + $GLOBALS['argv'][0], + 'issue', + '--server', $server, - "--storage", + '--storage', $storage, - "--domains", - implode(",", $domains), - "--path", + '--domains', + implode(',', $domains), + '--path', implode(PATH_SEPARATOR, array_values($domainPathMap)), ]; - if (isset($certificate["user"])) { - $args[] = "--user"; - $args[] = $certificate["user"]; + if (isset($certificate['user'])) { + $args[] = '--user'; + $args[] = $certificate['user']; } - if (isset($certificate["bits"])) { - $args[] = "--bits"; - $args[] = $certificate["bits"]; + if (isset($certificate['bits'])) { + $args[] = '--bits'; + $args[] = $certificate['bits']; } if ($concurrency) { - $args[] = "--challenge-concurrency"; + $args[] = '--challenge-concurrency'; $args[] = $concurrency; } - $command = implode(" ", array_map("escapeshellarg", $args)); + $process = new Process($args); + $process->start(); + $exit = yield $process->join(); - $process = new Process($command); - $result = (yield $process->exec(Process::BUFFER_ALL)); - - if ($result->exit !== 0) { - throw new AcmeException("Unexpected exit code ({$result->exit}) for '{$command}'." . PHP_EOL . $result->stdout . PHP_EOL . PHP_EOL . $result->stderr); + if ($exit !== 0) { + // TODO: Print STDOUT and STDERR to file + throw new AcmeException("Unexpected exit code ({$exit}) for '{$process->getCommand()}'."); } - yield new CoroutineResult(self::STATUS_RENEWED); - return; + return self::STATUS_RENEWED; } - throw new AcmeException("Unexpected exit code ({$result->exit}) for '{$command}'." . PHP_EOL . $result->stdout . PHP_EOL . PHP_EOL . $result->stderr); + // TODO: Print STDOUT and STDERR to file + throw new AcmeException("Unexpected exit code ({$exit}) for '{$process->getCommand()}'."); } private function toDomainPathMap(array $paths) { @@ -308,29 +303,29 @@ MESSAGE; return $result; } - public static function getDefinition() { - $server = \Kelunik\AcmeClient\getArgumentDescription("server"); - $storage = \Kelunik\AcmeClient\getArgumentDescription("storage"); + public static function getDefinition(): array { + $server = AcmeClient\getArgumentDescription('server'); + $storage = AcmeClient\getArgumentDescription('storage'); - $server["required"] = false; - $storage["required"] = false; + $server['required'] = false; + $storage['required'] = false; $args = [ - "server" => $server, - "storage" => $storage, - "config" => [ - "prefix" => "c", - "longPrefix" => "config", - "description" => "Configuration file to read.", - "required" => true, + 'server' => $server, + 'storage' => $storage, + 'config' => [ + 'prefix' => 'c', + 'longPrefix' => 'config', + 'description' => 'Configuration file to read.', + 'required' => true, ], ]; - $configPath = \Kelunik\AcmeClient\getConfigPath(); + $configPath = AcmeClient\getConfigPath(); if ($configPath) { - $args["config"]["required"] = false; - $args["config"]["defaultValue"] = $configPath; + $args['config']['required'] = false; + $args['config']['defaultValue'] = $configPath; } return $args; diff --git a/src/Commands/Check.php b/src/Commands/Check.php index a1bc930..90c6c24 100644 --- a/src/Commands/Check.php +++ b/src/Commands/Check.php @@ -2,12 +2,14 @@ namespace Kelunik\AcmeClient\Commands; -use Amp\CoroutineResult; +use Amp\Promise; +use Kelunik\AcmeClient; use Kelunik\AcmeClient\Stores\CertificateStore; use Kelunik\AcmeClient\Stores\CertificateStoreException; use Kelunik\Certificate\Certificate; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; +use function Amp\call; class Check implements Command { private $climate; @@ -16,76 +18,67 @@ class Check implements Command { $this->climate = $climate; } - public function execute(Manager $args) { - return \Amp\resolve($this->doExecute($args)); - } + public function execute(Manager $args): Promise { + return call(function () use ($args) { + $server = AcmeClient\resolveServer($args->get('server')); + $server = AcmeClient\serverToKeyname($server); - /** - * @param Manager $args - * @return \Generator - */ - private function doExecute(Manager $args) { - $server = \Kelunik\AcmeClient\resolveServer($args->get("server")); - $server = \Kelunik\AcmeClient\serverToKeyname($server); + $path = AcmeClient\normalizePath($args->get('storage')) . '/certs/' . $server; + $certificateStore = new CertificateStore($path); - $path = \Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $server; - $certificateStore = new CertificateStore($path); + try { + $pem = yield $certificateStore->get($args->get('name')); + } catch (CertificateStoreException $e) { + $this->climate->br()->error(' Certificate not found.')->br(); - try { - $pem = (yield $certificateStore->get($args->get("name"))); - } catch (CertificateStoreException $e) { - $this->climate->br()->error(" Certificate not found.")->br(); - - yield new CoroutineResult(1); - return; - } - - $cert = new Certificate($pem); - - $this->climate->br(); - $this->climate->whisper(" Certificate is valid until " . date("d.m.Y", $cert->getValidTo()))->br(); - - if ($args->defined("names")) { - $names = array_map("trim", explode(",", $args->get("names"))); - $missingNames = array_diff($names, $cert->getNames()); - - if ($missingNames) { - $this->climate->comment(" The following names are not covered: " . implode(", ", $missingNames))->br(); - - yield new CoroutineResult(1); - return; + return 1; } - } - if ($cert->getValidTo() > time() + $args->get("ttl") * 24 * 60 * 60) { - yield new CoroutineResult(0); - return; - } + $cert = new Certificate($pem); - $this->climate->comment(" Certificate is going to expire within the specified " . $args->get("ttl") . " days.")->br(); + $this->climate->br(); + $this->climate->whisper(' Certificate is valid until ' . date('d.m.Y', $cert->getValidTo()))->br(); - yield new CoroutineResult(1); + if ($args->defined('names')) { + $names = array_map('trim', explode(',', $args->get('names'))); + $missingNames = array_diff($names, $cert->getNames()); + + if ($missingNames) { + $this->climate->comment(' The following names are not covered: ' . implode(', ', $missingNames))->br(); + + return 1; + } + } + + if ($cert->getValidTo() > time() + $args->get('ttl') * 24 * 60 * 60) { + return 0; + } + + $this->climate->comment(' Certificate is going to expire within the specified ' . $args->get('ttl') . ' days.')->br(); + + return 1; + }); } - public static function getDefinition() { + public static function getDefinition(): array { return [ - "server" => \Kelunik\AcmeClient\getArgumentDescription("server"), - "storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"), - "name" => [ - "longPrefix" => "name", - "description" => "Common name of the certificate to check.", - "required" => true, + 'server' => \Kelunik\AcmeClient\getArgumentDescription('server'), + 'storage' => \Kelunik\AcmeClient\getArgumentDescription('storage'), + 'name' => [ + 'longPrefix' => 'name', + 'description' => 'Common name of the certificate to check.', + 'required' => true, ], - "ttl" => [ - "longPrefix" => "ttl", - "description" => "Minimum valid time in days.", - "defaultValue" => 30, - "castTo" => "int", + 'ttl' => [ + 'longPrefix' => 'ttl', + 'description' => 'Minimum valid time in days.', + 'defaultValue' => 30, + 'castTo' => 'int', ], - "names" => [ - "longPrefix" => "names", - "description" => "Names that must be covered by the certificate identified based on the common name. Names have to be separated by commas.", - "required" => false, + 'names' => [ + 'longPrefix' => 'names', + 'description' => 'Names that must be covered by the certificate identified based on the common name. Names have to be separated by commas.', + 'required' => false, ], ]; } diff --git a/src/Commands/Command.php b/src/Commands/Command.php index c151ba9..b505344 100644 --- a/src/Commands/Command.php +++ b/src/Commands/Command.php @@ -2,10 +2,11 @@ namespace Kelunik\AcmeClient\Commands; +use Amp\Promise; use League\CLImate\Argument\Manager; interface Command { - public function execute(Manager $args); + public function execute(Manager $args): Promise; - public static function getDefinition(); + public static function getDefinition(): array; } \ No newline at end of file diff --git a/src/Commands/Issue.php b/src/Commands/Issue.php index bdd69db..4b757e6 100644 --- a/src/Commands/Issue.php +++ b/src/Commands/Issue.php @@ -2,13 +2,15 @@ namespace Kelunik\AcmeClient\Commands; -use Amp\CoroutineResult; -use Amp\Dns\Record; -use Exception; +use Amp\Promise; use Kelunik\Acme\AcmeException; use Kelunik\Acme\AcmeService; -use Kelunik\Acme\KeyPair; -use Kelunik\Acme\OpenSSLKeyGenerator; +use Kelunik\Acme\Crypto\Backend\OpensslBackend; +use Kelunik\Acme\Crypto\PrivateKey; +use Kelunik\Acme\Crypto\RsaKeyGenerator; +use Kelunik\Acme\Csr\OpensslCsrGenerator; +use Kelunik\Acme\Verifiers\Http01; +use Kelunik\AcmeClient; use Kelunik\AcmeClient\AcmeFactory; use Kelunik\AcmeClient\Stores\CertificateStore; use Kelunik\AcmeClient\Stores\ChallengeStore; @@ -16,8 +18,8 @@ use Kelunik\AcmeClient\Stores\KeyStore; use Kelunik\AcmeClient\Stores\KeyStoreException; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; -use stdClass; -use Throwable; +use function Amp\call; +use function Kelunik\Acme\generateKeyAuthorization; class Issue implements Command { private $climate; @@ -28,113 +30,104 @@ class Issue implements Command { $this->acmeFactory = $acmeFactory; } - public function execute(Manager $args) { - return \Amp\resolve($this->doExecute($args)); - } + public function execute(Manager $args): Promise { + return call(function () use ($args) { + $user = null; - private function doExecute(Manager $args) { - if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - if (posix_geteuid() !== 0) { - $processUser = posix_getpwnam(posix_geteuid()); - $currentUsername = $processUser["name"]; - $user = $args->get("user") ?: $currentUsername; + if (0 !== stripos(PHP_OS, 'WIN')) { + if (posix_geteuid() !== 0) { + $processUser = posix_getpwnam(posix_geteuid()); + $currentUsername = $processUser['name']; + $user = $args->get('user') ?: $currentUsername; - if ($currentUsername !== $user) { - throw new AcmeException("Running this script with --user only works as root!"); + if ($currentUsername !== $user) { + throw new AcmeException('Running this script with --user only works as root!'); + } + } else { + $user = $args->get('user') ?: 'www-data'; } - } else { - $user = $args->get("user") ?: "www-data"; - } - } - - $domains = array_map("trim", explode(":", str_replace([",", ";"], ":", $args->get("domains")))); - yield \Amp\resolve($this->checkDnsRecords($domains)); - - $docRoots = explode(PATH_SEPARATOR, str_replace("\\", "/", $args->get("path"))); - $docRoots = array_map(function ($root) { - return rtrim($root, "/"); - }, $docRoots); - - if (count($domains) < count($docRoots)) { - throw new AcmeException("Specified more document roots than domains."); - } - - if (count($domains) > count($docRoots)) { - $docRoots = array_merge( - $docRoots, - array_fill(count($docRoots), count($domains) - count($docRoots), end($docRoots)) - ); - } - - $keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get("storage"))); - - $server = \Kelunik\AcmeClient\resolveServer($args->get("server")); - $keyFile = \Kelunik\AcmeClient\serverToKeyname($server); - - try { - $keyPair = (yield $keyStore->get("accounts/{$keyFile}.pem")); - } catch (KeyStoreException $e) { - throw new AcmeException("Account key not found, did you run 'bin/acme setup'?", 0, $e); - } - - $this->climate->br(); - - $acme = $this->acmeFactory->build($server, $keyPair); - $errors = []; - - $concurrency = $args->get("challenge-concurrency"); - - $domainChunks = array_chunk($domains, \min(20, \max($concurrency, 1)), true); - - foreach ($domainChunks as $domainChunk) { - $promises = []; - - foreach ($domainChunk as $i => $domain) { - $promises[] = \Amp\resolve($this->solveChallenge($acme, $keyPair, $domain, $docRoots[$i])); } - list($chunkErrors) = (yield \Amp\any($promises)); + $domains = array_map('trim', explode(':', str_replace([',', ';'], ':', $args->get('domains')))); + yield from $this->checkDnsRecords($domains); - $errors += $chunkErrors; - } + $docRoots = explode(PATH_SEPARATOR, str_replace("\\", '/', $args->get('path'))); + $docRoots = array_map(function ($root) { + return rtrim($root, '/'); + }, $docRoots); - if (!empty($errors)) { - foreach ($errors as $error) { - $this->climate->error($error->getMessage()); + if (\count($domains) < \count($docRoots)) { + throw new AcmeException('Specified more document roots than domains.'); } - throw new AcmeException("Issuance failed, not all challenges could be solved."); - } + if (\count($domains) > \count($docRoots)) { + $docRoots = array_merge( + $docRoots, + array_fill(\count($docRoots), \count($domains) - \count($docRoots), end($docRoots)) + ); + } - $path = "certs/" . $keyFile . "/" . reset($domains) . "/key.pem"; - $bits = $args->get("bits"); + $keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get('storage'))); - try { - $keyPair = (yield $keyStore->get($path)); - } catch (KeyStoreException $e) { - $keyPair = (new OpenSSLKeyGenerator)->generate($bits); - $keyPair = (yield $keyStore->put($path, $keyPair)); - } + $server = \Kelunik\AcmeClient\resolveServer($args->get('server')); + $keyFile = \Kelunik\AcmeClient\serverToKeyname($server); - $this->climate->br(); - $this->climate->whisper(" Requesting certificate ..."); + try { + $key = yield $keyStore->get("accounts/{$keyFile}.pem"); + } catch (KeyStoreException $e) { + throw new AcmeException("Account key not found, did you run 'bin/acme setup'?", 0, $e); + } - $location = (yield $acme->requestCertificate($keyPair, $domains)); - $certificates = (yield $acme->pollForCertificate($location)); + $this->climate->br(); - $path = \Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $keyFile; - $certificateStore = new CertificateStore($path); - yield $certificateStore->put($certificates); + $acme = $this->acmeFactory->build($server, $key); + $concurrency = \min(20, \max($args->get('challenge-concurrency'), 1)); - $this->climate->info(" Successfully issued certificate."); - $this->climate->info(" See {$path}/" . reset($domains)); - $this->climate->br(); + /** @var \Throwable[] $errors */ + list($errors) = yield AcmeClient\concurrentMap($concurrency, $domains, function ($domain, $i) use ($acme, $key, $docRoots, $user) { + return $this->solveChallenge($acme, $key, $domain, $docRoots[$i], $user); + }); - yield new CoroutineResult(0); + if ($errors) { + foreach ($errors as $error) { + $this->climate->error($error->getMessage()); + } + + throw new AcmeException('Issuance failed, not all challenges could be solved.'); + } + + $path = 'certs/' . $keyFile . '/' . reset($domains) . '/key.pem'; + $bits = $args->get('bits'); + + try { + $key = yield $keyStore->get($path); + } catch (KeyStoreException $e) { + $key = (new RsaKeyGenerator($bits))->generateKey(); + $key = yield $keyStore->put($path, $key); + } + + $this->climate->br(); + $this->climate->whisper(' Requesting certificate ...'); + + $csr = (new OpensslCsrGenerator)->generateCsr($key, $domains); + + $location = yield $acme->requestCertificate($csr); + $certificates = yield $acme->pollForCertificate($location); + + $path = AcmeClient\normalizePath($args->get('storage')) . '/certs/' . $keyFile; + $certificateStore = new CertificateStore($path); + yield $certificateStore->put($certificates); + + $this->climate->info(' Successfully issued certificate.'); + $this->climate->info(" See {$path}/" . reset($domains)); + $this->climate->br(); + + return 0; + }); } - private function solveChallenge(AcmeService $acme, KeyPair $keyPair, $domain, $path) { - list($location, $challenges) = (yield $acme->requestChallenges($domain)); + private function solveChallenge(AcmeService $acme, PrivateKey $key, string $domain, string $path, string $user = null): \Generator { + list($location, $challenges) = yield $acme->requestChallenges($domain); $goodChallenges = $this->findSuitableCombination($challenges); if (empty($goodChallenges)) { @@ -144,81 +137,57 @@ class Issue implements Command { $challenge = $challenges->challenges[reset($goodChallenges)]; $token = $challenge->token; - if (!preg_match("#^[a-zA-Z0-9-_]+$#", $token)) { - throw new AcmeException("Protocol violation: Invalid Token!"); + if (!preg_match('#^[a-zA-Z0-9-_]+$#', $token)) { + throw new AcmeException('Protocol violation: Invalid Token!'); } - $payload = $acme->generateHttp01Payload($keyPair, $token); + $payload = generateKeyAuthorization($key, $token, new OpensslBackend); $this->climate->whisper(" Providing payload at http://{$domain}/.well-known/acme-challenge/{$token}"); $challengeStore = new ChallengeStore($path); try { - yield $challengeStore->put($token, $payload, isset($user) ? $user : null); + yield $challengeStore->put($token, $payload, $user); - yield $acme->verifyHttp01Challenge($domain, $token, $payload); + yield (new Http01)->verifyChallenge($domain, $token, $payload); yield $acme->answerChallenge($challenge->uri, $payload); yield $acme->pollForChallenge($location); $this->climate->comment(" {$domain} is now authorized."); - + } finally { yield $challengeStore->delete($token); - } catch (Exception $e) { - // no finally because generators... - yield $challengeStore->delete($token); - throw $e; - } catch (Throwable $e) { - // no finally because generators... - yield $challengeStore->delete($token); - throw $e; } } - private function checkDnsRecords($domains) { - $errors = []; + private function checkDnsRecords(array $domains): \Generator { + $promises = AcmeClient\concurrentMap(10, \array_combine($domains, $domains), 'Amp\Dns\resolve'); + list($errors) = yield Promise\any($promises); - $domainChunks = array_chunk($domains, 10, true); - - foreach ($domainChunks as $domainChunk) { - $promises = []; - - foreach ($domainChunk as $domain) { - $promises[$domain] = \Amp\Dns\resolve($domain, [ - "types" => [Record::A, Record::AAAA], - "hosts" => false, - ]); - } - - list($chunkErrors) = (yield \Amp\any($promises)); - - $errors += $chunkErrors; - } - - if (!empty($errors)) { - $failedDomains = implode(", ", array_keys($errors)); + if ($errors) { + $failedDomains = implode(', ', array_keys($errors)); $reasons = implode("\n\n", array_map(function ($exception) { - /** @var \Exception|\Throwable $exception */ - return get_class($exception) . ": " . $exception->getMessage(); + /** @var \Throwable $exception */ + return \get_class($exception) . ': ' . $exception->getMessage(); }, $errors)); throw new AcmeException("Couldn't resolve the following domains to an IPv4 nor IPv6 record: {$failedDomains}\n\n{$reasons}"); } } - private function findSuitableCombination(stdClass $response) { - $challenges = isset($response->challenges) ? $response->challenges : []; - $combinations = isset($response->combinations) ? $response->combinations : []; + private function findSuitableCombination(\stdClass $response): array { + $challenges = $response->challenges ?? []; + $combinations = $response->combinations ?? []; $goodChallenges = []; foreach ($challenges as $i => $challenge) { - if ($challenge->type === "http-01") { + if ($challenge->type === 'http-01') { $goodChallenges[] = $i; } } foreach ($goodChallenges as $i => $challenge) { - if (!in_array([$challenge], $combinations)) { + if (!\in_array([$challenge], $combinations, true)) { unset($goodChallenges[$i]); } } @@ -226,38 +195,38 @@ class Issue implements Command { return $goodChallenges; } - public static function getDefinition() { + public static function getDefinition(): array { return [ - "server" => \Kelunik\AcmeClient\getArgumentDescription("server"), - "storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"), - "domains" => [ - "prefix" => "d", - "longPrefix" => "domains", - "description" => "Colon / Semicolon / Comma separated list of domains to request a certificate for.", - "required" => true, + 'server' => AcmeClient\getArgumentDescription('server'), + 'storage' => AcmeClient\getArgumentDescription('storage'), + 'domains' => [ + 'prefix' => 'd', + 'longPrefix' => 'domains', + 'description' => 'Colon / Semicolon / Comma separated list of domains to request a certificate for.', + 'required' => true, ], - "path" => [ - "prefix" => "p", - "longPrefix" => "path", - "description" => "Colon (Unix) / Semicolon (Windows) separated list of paths to the document roots. The last one will be used for all remaining ones if fewer than the amount of domains is given.", - "required" => true, + 'path' => [ + 'prefix' => 'p', + 'longPrefix' => 'path', + 'description' => 'Colon (Unix) / Semicolon (Windows) separated list of paths to the document roots. The last one will be used for all remaining ones if fewer than the amount of domains is given.', + 'required' => true, ], - "user" => [ - "prefix" => "u", - "longPrefix" => "user", - "description" => "User running the web server.", + 'user' => [ + 'prefix' => 'u', + 'longPrefix' => 'user', + 'description' => 'User running the web server.', ], - "bits" => [ - "longPrefix" => "bits", - "description" => "Length of the private key in bit.", - "defaultValue" => 2048, - "castTo" => "int", + 'bits' => [ + 'longPrefix' => 'bits', + 'description' => 'Length of the private key in bit.', + 'defaultValue' => 2048, + 'castTo' => 'int', ], - "challenge-concurrency" => [ - "longPrefix" => "challenge-concurrency", - "description" => "Number of challenges to be solved concurrently.", - "defaultValue" => 10, - "castTo" => "int", + 'challenge-concurrency' => [ + 'longPrefix' => 'challenge-concurrency', + 'description' => 'Number of challenges to be solved concurrently.', + 'defaultValue' => 10, + 'castTo' => 'int', ], ]; } diff --git a/src/Commands/Revoke.php b/src/Commands/Revoke.php index 1c4e2f3..ebc3709 100644 --- a/src/Commands/Revoke.php +++ b/src/Commands/Revoke.php @@ -2,14 +2,17 @@ namespace Kelunik\AcmeClient\Commands; -use Amp\CoroutineResult; +use Amp\File; use Amp\File\FilesystemException; +use Amp\Promise; +use Kelunik\AcmeClient; use Kelunik\AcmeClient\AcmeFactory; use Kelunik\AcmeClient\Stores\CertificateStore; use Kelunik\AcmeClient\Stores\KeyStore; use Kelunik\Certificate\Certificate; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; +use function Amp\call; class Revoke implements Command { private $climate; @@ -20,57 +23,55 @@ class Revoke implements Command { $this->acmeFactory = $acmeFactory; } - public function execute(Manager $args) { - return \Amp\resolve($this->doExecute($args)); + public function execute(Manager $args): Promise { + return call(function () use ($args) { + $keyStore = new KeyStore(AcmeClient\normalizePath($args->get('storage'))); + + $server = AcmeClient\resolveServer($args->get('server')); + $keyFile = AcmeClient\serverToKeyname($server); + + $keyPair = yield $keyStore->get("accounts/{$keyFile}.pem"); + $acme = $this->acmeFactory->build($server, $keyPair); + + $this->climate->br(); + $this->climate->whisper(' Revoking certificate ...'); + + $path = AcmeClient\normalizePath($args->get('storage')) . '/certs/' . $keyFile . '/' . $args->get('name') . '/cert.pem'; + + try { + $pem = yield File\get($path); + $cert = new Certificate($pem); + } catch (FilesystemException $e) { + throw new \RuntimeException("There's no such certificate (" . $path . ')'); + } + + if ($cert->getValidTo() < time()) { + $this->climate->comment(' Certificate did already expire, no need to revoke it.'); + } + + $names = $cert->getNames(); + $this->climate->whisper(' Certificate was valid for ' . \count($names) . ' domains.'); + $this->climate->whisper(' - ' . implode(PHP_EOL . ' - ', $names) . PHP_EOL); + + yield $acme->revokeCertificate($pem); + + $this->climate->br(); + $this->climate->info(' Certificate has been revoked.'); + + yield (new CertificateStore(AcmeClient\normalizePath($args->get('storage')) . '/certs/' . $keyFile))->delete($args->get('name')); + + return 0; + }); } - private function doExecute(Manager $args) { - $keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get("storage"))); - - $server = \Kelunik\AcmeClient\resolveServer($args->get("server")); - $keyFile = \Kelunik\AcmeClient\serverToKeyname($server); - - $keyPair = (yield $keyStore->get("accounts/{$keyFile}.pem")); - $acme = $this->acmeFactory->build($server, $keyPair); - - $this->climate->br(); - $this->climate->whisper(" Revoking certificate ..."); - - $path = \Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $keyFile . "/" . $args->get("name") . "/cert.pem"; - - try { - $pem = (yield \Amp\File\get($path)); - $cert = new Certificate($pem); - } catch (FilesystemException $e) { - throw new \RuntimeException("There's no such certificate (" . $path . ")"); - } - - if ($cert->getValidTo() < time()) { - $this->climate->comment(" Certificate did already expire, no need to revoke it."); - } - - $names = $cert->getNames(); - $this->climate->whisper(" Certificate was valid for " . count($names) . " domains."); - $this->climate->whisper(" - " . implode(PHP_EOL . " - ", $names) . PHP_EOL); - - yield $acme->revokeCertificate($pem); - - $this->climate->br(); - $this->climate->info(" Certificate has been revoked."); - - yield (new CertificateStore(\Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $keyFile))->delete($args->get("name")); - - yield new CoroutineResult(0); - } - - public static function getDefinition() { + public static function getDefinition(): array { return [ - "server" => \Kelunik\AcmeClient\getArgumentDescription("server"), - "storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"), - "name" => [ - "longPrefix" => "name", - "description" => "Common name of the certificate to be revoked.", - "required" => true, + 'server' => AcmeClient\getArgumentDescription('server'), + 'storage' => AcmeClient\getArgumentDescription('storage'), + 'name' => [ + 'longPrefix' => 'name', + 'description' => 'Common name of the certificate to be revoked.', + 'required' => true, ], ]; } diff --git a/src/Commands/Setup.php b/src/Commands/Setup.php index c40bf51..d3d13c0 100644 --- a/src/Commands/Setup.php +++ b/src/Commands/Setup.php @@ -2,20 +2,22 @@ namespace Kelunik\AcmeClient\Commands; -use Amp\CoroutineResult; +use Amp\Dns; use Amp\Dns\NoRecordException; use Amp\Dns\Record; use Amp\Dns\ResolutionException; -use InvalidArgumentException; +use Amp\Promise; use Kelunik\Acme\AcmeException; -use Kelunik\Acme\OpenSSLKeyGenerator; +use Kelunik\Acme\Crypto\RsaKeyGenerator; use Kelunik\Acme\Registration; +use Kelunik\AcmeClient; use Kelunik\AcmeClient\AcmeFactory; use Kelunik\AcmeClient\Stores\KeyStore; use Kelunik\AcmeClient\Stores\KeyStoreException; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; use Symfony\Component\Yaml\Yaml; +use function Amp\call; class Setup implements Command { private $climate; @@ -26,61 +28,55 @@ class Setup implements Command { $this->acmeFactory = $acmeFactory; } - public function execute(Manager $args) { - return \Amp\resolve($this->doExecute($args)); + public function execute(Manager $args): Promise { + return call(function () use ($args) { + $email = $args->get('email'); + yield from $this->checkEmail($email); + + $server = AcmeClient\resolveServer($args->get('server')); + $keyFile = AcmeClient\serverToKeyname($server); + + $path = "accounts/{$keyFile}.pem"; + $bits = 4096; + + $keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get('storage'))); + + $this->climate->br(); + + try { + $keyPair = yield $keyStore->get($path); + $this->climate->whisper(' Using existing private key ...'); + } catch (KeyStoreException $e) { + $this->climate->whisper(' No private key found, generating new one ...'); + + $keyPair = (new RsaKeyGenerator($bits))->generateKey(); + $keyPair = yield $keyStore->put($path, $keyPair); + + $this->climate->whisper(" Generated new private key with {$bits} bits."); + } + + $acme = $this->acmeFactory->build($server, $keyPair); + + $this->climate->whisper(' Registering with ' . substr($server, 8) . ' ...'); + + /** @var Registration $registration */ + $registration = yield $acme->register($email); + $this->climate->info(' Registration successful. Contacts: ' . implode(', ', $registration->getContact())); + $this->climate->br(); + + return 0; + }); } - public function doExecute(Manager $args) { - $email = $args->get("email"); - yield \Amp\resolve($this->checkEmail($email)); - - $server = \Kelunik\AcmeClient\resolveServer($args->get("server")); - $keyFile = \Kelunik\AcmeClient\serverToKeyname($server); - - $path = "accounts/{$keyFile}.pem"; - $bits = 4096; - - $keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get("storage"))); - - $this->climate->br(); - - try { - $keyPair = (yield $keyStore->get($path)); - $this->climate->whisper(" Using existing private key ..."); - } catch (KeyStoreException $e) { - $this->climate->whisper(" No private key found, generating new one ..."); - - $keyPair = (new OpenSSLKeyGenerator)->generate($bits); - $keyPair = (yield $keyStore->put($path, $keyPair)); - - $this->climate->whisper(" Generated new private key with {$bits} bits."); - } - - $acme = $this->acmeFactory->build($server, $keyPair); - - $this->climate->whisper(" Registering with " . substr($server, 8) . " ..."); - - /** @var Registration $registration */ - $registration = (yield $acme->register($email)); - $this->climate->info(" Registration successful. Contacts: " . implode(", ", $registration->getContact())); - $this->climate->br(); - - yield new CoroutineResult(0); - } - - private function checkEmail($email) { - if (!is_string($email)) { - throw new InvalidArgumentException(sprintf("\$email must be of type string, %s given.", gettype($email))); - } - - $host = substr($email, strrpos($email, "@") + 1); + private function checkEmail(string $email) { + $host = substr($email, strrpos($email, '@') + 1); if (!$host) { throw new AcmeException("Invalid contact email: '{$email}'"); } try { - yield \Amp\Dns\query($host, Record::MX); + yield Dns\query($host, Record::MX); } catch (NoRecordException $e) { throw new AcmeException("No MX record defined for '{$host}'"); } catch (ResolutionException $e) { @@ -88,25 +84,25 @@ class Setup implements Command { } } - public static function getDefinition() { + public static function getDefinition(): array { $args = [ - "server" => \Kelunik\AcmeClient\getArgumentDescription("server"), - "storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"), - "email" => [ - "longPrefix" => "email", - "description" => "E-mail for important issues, will be sent to the ACME server.", - "required" => true, + 'server' => AcmeClient\getArgumentDescription('server'), + 'storage' => AcmeClient\getArgumentDescription('storage'), + 'email' => [ + 'longPrefix' => 'email', + 'description' => 'E-mail for important issues, will be sent to the ACME server.', + 'required' => true, ], ]; - $configPath = \Kelunik\AcmeClient\getConfigPath(); + $configPath = AcmeClient\getConfigPath(); if ($configPath) { $config = Yaml::parse(file_get_contents($configPath)); - if (isset($config["email"]) && is_string($config["email"])) { - $args["email"]["required"] = false; - $args["email"]["defaultValue"] = $config["email"]; + if (isset($config['email']) && \is_string($config['email'])) { + $args['email']['required'] = false; + $args['email']['defaultValue'] = $config['email']; } } diff --git a/src/Commands/Status.php b/src/Commands/Status.php index 4931aac..1b6bf2b 100644 --- a/src/Commands/Status.php +++ b/src/Commands/Status.php @@ -2,12 +2,16 @@ namespace Kelunik\AcmeClient\Commands; +use Amp\File; +use Amp\Promise; use Kelunik\AcmeClient\Stores\CertificateStore; use Kelunik\AcmeClient\Stores\KeyStore; use Kelunik\AcmeClient\Stores\KeyStoreException; use Kelunik\Certificate\Certificate; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; +use function Amp\call; +use function Kelunik\AcmeClient\getArgumentDescription; class Status { private $climate; @@ -16,64 +20,59 @@ class Status { $this->climate = $climate; } - public function execute(Manager $args) { - return \Amp\resolve($this->doExecute($args)); - } + public function execute(Manager $args): Promise { + return call(function () use ($args) { + $server = \Kelunik\AcmeClient\resolveServer($args->get('server')); + $keyName = \Kelunik\AcmeClient\serverToKeyname($server); - /** - * @param Manager $args - * @return \Generator - */ - private function doExecute(Manager $args) { - $server = \Kelunik\AcmeClient\resolveServer($args->get("server")); - $keyName = \Kelunik\AcmeClient\serverToKeyname($server); + $storage = \Kelunik\AcmeClient\normalizePath($args->get('storage')); - $storage = \Kelunik\AcmeClient\normalizePath($args->get("storage")); + try { + $keyStore = new KeyStore($storage); + yield $keyStore->get("accounts/{$keyName}.pem"); - try { - $keyStore = new KeyStore($storage); - yield $keyStore->get("accounts/{$keyName}.pem"); - - $setup = true; - } catch (KeyStoreException $e) { - $setup = false; - } - - $this->climate->br(); - $this->climate->out(" [" . ($setup ? "" : "") . "] " . ($setup ? "Registered on " : "Not yet registered on ") . $server); - $this->climate->br(); - - if (yield \Amp\File\exists($storage . "/certs/{$keyName}")) { - $certificateStore = new CertificateStore($storage . "/certs/{$keyName}"); - - $domains = (yield \Amp\File\scandir($storage . "/certs/{$keyName}")); - - foreach ($domains as $domain) { - $pem = (yield $certificateStore->get($domain)); - $cert = new Certificate($pem); - - $symbol = time() > $cert->getValidTo() ? "" : ""; - - if (time() < $cert->getValidTo() && time() + $args->get("ttl") * 24 * 60 * 60 > $cert->getValidTo()) { - $symbol = ""; - } - - $this->climate->out(" [" . $symbol . "] " . implode(", ", $cert->getNames())); + $setup = true; + } catch (KeyStoreException $e) { + $setup = false; } $this->climate->br(); - } + $this->climate->out(' [' . ($setup ? '' : '') . '] ' . ($setup ? 'Registered on ' : 'Not yet registered on ') . $server); + $this->climate->br(); + + if (yield File\exists($storage . "/certs/{$keyName}")) { + $certificateStore = new CertificateStore($storage . "/certs/{$keyName}"); + + /** @var array $domains */ + $domains = yield File\scandir($storage . "/certs/{$keyName}"); + + foreach ($domains as $domain) { + $pem = yield $certificateStore->get($domain); + $cert = new Certificate($pem); + + $symbol = time() > $cert->getValidTo() ? '' : ''; + + if (time() < $cert->getValidTo() && time() + $args->get('ttl') * 24 * 60 * 60 > $cert->getValidTo()) { + $symbol = ''; + } + + $this->climate->out(' [' . $symbol . '] ' . implode(', ', $cert->getNames())); + } + + $this->climate->br(); + } + }); } - public static function getDefinition() { + public static function getDefinition(): array { return [ - "server" => \Kelunik\AcmeClient\getArgumentDescription("server"), - "storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"), - "ttl" => [ - "longPrefix" => "ttl", - "description" => "Minimum valid time in days, shows ⭮ if renewal is required.", - "defaultValue" => 30, - "castTo" => "int", + 'server' => getArgumentDescription('server'), + 'storage' => getArgumentDescription('storage'), + 'ttl' => [ + 'longPrefix' => 'ttl', + 'description' => 'Minimum valid time in days, shows ⭮ if renewal is required.', + 'defaultValue' => 30, + 'castTo' => 'int', ], ]; } diff --git a/src/Commands/Version.php b/src/Commands/Version.php index 1ab87c3..8a87201 100644 --- a/src/Commands/Version.php +++ b/src/Commands/Version.php @@ -2,9 +2,10 @@ namespace Kelunik\AcmeClient\Commands; +use Amp\Promise; +use Amp\Success; use League\CLImate\Argument\Manager; use League\CLImate\CLImate; -use RuntimeException; class Version implements Command { private $climate; @@ -13,63 +14,65 @@ class Version implements Command { $this->climate = $climate; } - public function execute(Manager $args) { + public function execute(Manager $args): Promise { $version = $this->getVersion(); - $buildTime = $this->readFileOr("info/build.time", time()); + $buildTime = $this->readFileOr('info/build.time', time()); $buildDate = date('M jS Y H:i:s T', (int) trim($buildTime)); - $package = json_decode($this->readFileOr("composer.json", new RuntimeException("No composer.json found."))); + $package = json_decode($this->readFileOr('composer.json', new \Exception('No composer.json found.'))); $this->climate->out("┌ kelunik/acme-client @ {$version} (built: {$buildDate})"); - $this->climate->out(($args->defined("deps") ? "│" : "└") . " " . $this->getDescription($package)); + $this->climate->out(($args->defined('deps') ? '│' : '└') . ' ' . $this->getDescription($package)); - if ($args->defined("deps")) { - $lockFile = json_decode($this->readFileOr("composer.lock", new RuntimeException("No composer.lock found."))); + if ($args->defined('deps')) { + $lockFile = json_decode($this->readFileOr('composer.lock', new \Exception('No composer.lock found.'))); $packages = $lockFile->packages; - for ($i = 0; $i < count($packages); $i++) { - $link = $i === count($packages) - 1 ? "└──" : "├──"; + for ($i = 0, $count = \count($packages); $i < $count; $i++) { + $link = $i === $count - 1 ? '└──' : '├──'; $this->climate->out("{$link} {$packages[$i]->name} @ {$packages[$i]->version}"); - $link = $i === count($packages) - 1 ? " " : "│ "; + $link = $i === $count - 1 ? ' ' : '│ '; $this->climate->out("{$link} " . $this->getDescription($packages[$i])); } } + + return new Success; } private function getDescription($package) { - return \Kelunik\AcmeClient\ellipsis(isset($package->description) ? $package->description : ""); + return \Kelunik\AcmeClient\ellipsis($package->description ?? ''); } private function getVersion() { - if (file_exists(__DIR__ . "/../../.git")) { + if (file_exists(__DIR__ . '/../../.git')) { $version = `git describe --tags`; } else { - $version = $this->readFileOr("info/build.version", "-unknown"); + $version = $this->readFileOr('info/build.version', '-unknown'); } return substr(trim($version), 1); } - private function readFileOr($file, $default = "") { - if (file_exists(__DIR__ . "/../../" . $file)) { - return file_get_contents(__DIR__ . "/../../" . $file); - } else { - if ($default instanceof \Exception || $default instanceof \Throwable) { - throw $default; - } - - return $default; + private function readFileOr($file, $default = '') { + if (file_exists(__DIR__ . '/../../' . $file)) { + return file_get_contents(__DIR__ . '/../../' . $file); } + + if ($default instanceof \Throwable) { + throw $default; + } + + return $default; } - public static function getDefinition() { + public static function getDefinition(): array { return [ - "deps" => [ - "longPrefix" => "deps", - "description" => "Show also the bundled dependency versions.", - "noValue" => true, + 'deps' => [ + 'longPrefix' => 'deps', + 'description' => 'Show also the bundled dependency versions.', + 'noValue' => true, ], ]; } diff --git a/src/ConfigException.php b/src/ConfigException.php index e7b9308..f1f88bb 100644 --- a/src/ConfigException.php +++ b/src/ConfigException.php @@ -2,4 +2,5 @@ namespace Kelunik\AcmeClient; -class ConfigException extends \Exception { } \ No newline at end of file +class ConfigException extends \Exception { +} \ No newline at end of file diff --git a/src/Stores/CertificateStore.php b/src/Stores/CertificateStore.php index 42f6033..6404cd1 100644 --- a/src/Stores/CertificateStore.php +++ b/src/Stores/CertificateStore.php @@ -2,92 +2,79 @@ namespace Kelunik\AcmeClient\Stores; -use Amp\CoroutineResult; +use Amp\File; use Amp\File\FilesystemException; -use InvalidArgumentException; +use Amp\Promise; use Kelunik\Certificate\Certificate; -use Webmozart\Assert\Assert; +use function Amp\call; +use function Amp\Uri\isValidDnsName; class CertificateStore { private $root; - public function __construct($root) { - if (!is_string($root)) { - throw new InvalidArgumentException(sprintf("\$root must be of type string, %s given.", gettype($root))); - } - - $this->root = rtrim(str_replace("\\", "/", $root), "/"); + public function __construct(string $root) { + $this->root = rtrim(str_replace("\\", '/', $root), '/'); } - public function get($name) { - return \Amp\resolve($this->doGet($name)); + public function get(string $name): Promise { + return call(function () use ($name) { + try { + return yield File\get($this->root . '/' . $name . '/cert.pem'); + } catch (FilesystemException $e) { + throw new CertificateStoreException('Failed to load certificate.', 0, $e); + } + }); } - private function doGet($name) { - Assert::string($name, "Name must be a string. Got: %s"); - - try { - $contents = (yield \Amp\File\get($this->root . "/" . $name . "/cert.pem")); - yield new CoroutineResult($contents); - } catch (FilesystemException $e) { - throw new CertificateStoreException("Failed to load certificate.", 0, $e); - } - } - - public function put(array $certificates) { - return \Amp\resolve($this->doPut($certificates)); - } - - private function doPut(array $certificates) { - if (empty($certificates)) { - throw new InvalidArgumentException("Empty array not allowed"); - } - - $cert = new Certificate($certificates[0]); - $commonName = $cert->getSubject()->getCommonName(); - - if (!$commonName) { - throw new CertificateStoreException("Certificate doesn't have a common name."); - } - - // See https://github.com/amphp/dns/blob/4c4d450d4af26fc55dc56dcf45ec7977373a38bf/lib/functions.php#L83 - if (isset($commonName[253]) || !preg_match("~^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9]){0,1})(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$~i", $commonName)) { - throw new CertificateStoreException("Invalid common name: '{$commonName}'"); - } - - try { - $chain = array_slice($certificates, 1); - $path = $this->root . "/" . $commonName; - $realpath = realpath($path); - - if (!$realpath && !mkdir($path, 0775, true)) { - throw new FilesystemException("Couldn't create certificate directory: '{$path}'"); + public function put(array $certificates): Promise { + return call(function () use ($certificates) { + if (empty($certificates)) { + throw new \Error('Empty array not allowed'); } - yield \Amp\File\put($path . "/cert.pem", $certificates[0]); - yield \Amp\File\chmod($path . "/cert.pem", 0644); + $cert = new Certificate($certificates[0]); + $commonName = $cert->getSubject()->getCommonName(); - yield \Amp\File\put($path . "/fullchain.pem", implode("\n", $certificates)); - yield \Amp\File\chmod($path . "/fullchain.pem", 0644); + if (!$commonName) { + throw new CertificateStoreException("Certificate doesn't have a common name."); + } - yield \Amp\File\put($path . "/chain.pem", implode("\n", $chain)); - yield \Amp\File\chmod($path . "/chain.pem", 0644); - } catch (FilesystemException $e) { - throw new CertificateStoreException("Couldn't save certificates for '{$commonName}'", 0, $e); - } + if (!isValidDnsName($commonName)) { + throw new CertificateStoreException("Invalid common name: '{$commonName}'"); + } + + try { + $chain = \array_slice($certificates, 1); + $path = $this->root . '/' . $commonName; + + if (!yield File\isdir($path) && !yield File\mkdir($path, 0644, true) && !yield File\isdir($path)) { + throw new FilesystemException("Couldn't create certificate directory: '{$path}'"); + } + + yield File\put($path . '/cert.pem', $certificates[0]); + yield File\chmod($path . '/cert.pem', 0644); + + yield File\put($path . '/fullchain.pem', implode("\n", $certificates)); + yield File\chmod($path . '/fullchain.pem', 0644); + + yield File\put($path . '/chain.pem', implode("\n", $chain)); + yield File\chmod($path . '/chain.pem', 0644); + } catch (FilesystemException $e) { + throw new CertificateStoreException("Couldn't save certificates for '{$commonName}'", 0, $e); + } + }); } - public function delete($name) { - return \Amp\resolve($this->doDelete($name)); - } + public function delete(string $name): Promise { + return call(function () use ($name) { + /** @var array $files */ + $files = yield File\scandir($this->root . '/' . $name); - private function doDelete($name) { - Assert::string($name, "Name must be a string. Got: %s"); + foreach ($files as $file) { + yield File\unlink($this->root . '/' . $name . '/' . $file); + } - foreach ((yield \Amp\File\scandir($this->root . "/" . $name)) as $file) { - yield \Amp\File\unlink($this->root . "/" . $name . "/" . $file); - } - - yield \Amp\File\rmdir($this->root . "/" . $name); + yield File\rmdir($this->root . '/' . $name); + }); } } diff --git a/src/Stores/CertificateStoreException.php b/src/Stores/CertificateStoreException.php index 4ead16f..7d51bf3 100644 --- a/src/Stores/CertificateStoreException.php +++ b/src/Stores/CertificateStoreException.php @@ -2,8 +2,5 @@ namespace Kelunik\AcmeClient\Stores; -use RuntimeException; - -class CertificateStoreException extends RuntimeException { - +class CertificateStoreException extends \Exception { } \ No newline at end of file diff --git a/src/Stores/ChallengeStore.php b/src/Stores/ChallengeStore.php index b9a9b1e..d0598c7 100644 --- a/src/Stores/ChallengeStore.php +++ b/src/Stores/ChallengeStore.php @@ -2,72 +2,56 @@ namespace Kelunik\AcmeClient\Stores; -use InvalidArgumentException; -use Webmozart\Assert\Assert; +use Amp\File; +use Amp\Promise; +use function Amp\call; class ChallengeStore { private $docroot; - public function __construct($docroot) { - if (!is_string($docroot)) { - throw new InvalidArgumentException(sprintf("\$docroot must be of type string, %s given.", gettype($docroot))); - } - - $this->docroot = rtrim(str_replace("\\", "/", $docroot), "/"); + public function __construct(string $docroot) { + $this->docroot = rtrim(str_replace("\\", '/', $docroot), '/'); } - public function put($token, $payload, $user = null) { - return \Amp\resolve($this->doPut($token, $payload, $user)); - } + public function put(string $token, string $payload, string $user = null): Promise { + return call(function () use ($token, $payload, $user) { + $path = $this->docroot . '/.well-known/acme-challenge'; + $userInfo = null; - private function doPut($token, $payload, $user = null) { - Assert::string($token, "Token must be a string. Got: %s"); - Assert::string($payload, "Payload must be a string. Got: %s"); - Assert::nullOrString($user, "User must be a string or null. Got: %s"); + if (!yield File\exists($this->docroot)) { + throw new ChallengeStoreException("Document root doesn't exist: '{$this->docroot}'"); + } - $path = $this->docroot . "/.well-known/acme-challenge"; - $realpath = realpath($path); + if (!yield File\isdir($path) && !yield File\mkdir($path, 0644, true) && !yield File\isdir($path)) { + throw new ChallengeStoreException("Couldn't create key directory: '{$path}'"); + } - if (!realpath($this->docroot)) { - throw new ChallengeStoreException("Document root doesn't exist: '{$this->docroot}'"); - } - - if (!$realpath && !@mkdir($path, 0755, true)) { - throw new ChallengeStoreException("Couldn't create public directory to serve the challenges: '{$path}'"); - } - - if ($user) { - if (!$userInfo = posix_getpwnam($user)) { + if ($user && !$userInfo = posix_getpwnam($user)) { throw new ChallengeStoreException("Unknown user: '{$user}'"); } - } - if (isset($userInfo)) { - yield \Amp\File\chown($this->docroot . "/.well-known", $userInfo["uid"], -1); - yield \Amp\File\chown($this->docroot . "/.well-known/acme-challenge", $userInfo["uid"], -1); - } + if ($userInfo !== null) { + yield File\chown($this->docroot . '/.well-known', $userInfo['uid'], -1); + yield File\chown($this->docroot . '/.well-known/acme-challenge', $userInfo['uid'], -1); + } - yield \Amp\File\put("{$path}/{$token}", $payload); + yield \Amp\File\put("{$path}/{$token}", $payload); - if (isset($userInfo)) { - yield \Amp\File\chown("{$path}/{$token}", $userInfo["uid"], -1); - } + if ($userInfo !== null) { + yield \Amp\File\chown("{$path}/{$token}", $userInfo['uid'], -1); + } - yield \Amp\File\chmod("{$path}/{$token}", 0644); + yield \Amp\File\chmod("{$path}/{$token}", 0644); + }); } - public function delete($token) { - return \Amp\resolve($this->doDelete($token)); - } + public function delete(string $token): Promise { + return call(function () use ($token) { + $path = $this->docroot . "/.well-known/acme-challenge/{$token}"; - private function doDelete($token) { - Assert::string($token, "Token must be a string. Got: %s"); - - $path = $this->docroot . "/.well-known/acme-challenge/{$token}"; - $realpath = realpath($path); - - if ($realpath) { - yield \Amp\File\unlink($realpath); - } + if (yield File\exists($path)) { + yield \Amp\File\unlink($path); + } + }); } } \ No newline at end of file diff --git a/src/Stores/ChallengeStoreException.php b/src/Stores/ChallengeStoreException.php index 9b754e9..fc7356b 100644 --- a/src/Stores/ChallengeStoreException.php +++ b/src/Stores/ChallengeStoreException.php @@ -2,8 +2,5 @@ namespace Kelunik\AcmeClient\Stores; -use RuntimeException; - -class ChallengeStoreException extends RuntimeException { - +class ChallengeStoreException extends \Exception { } \ No newline at end of file diff --git a/src/Stores/KeyStore.php b/src/Stores/KeyStore.php index 78a6e21..ffa0f16 100644 --- a/src/Stores/KeyStore.php +++ b/src/Stores/KeyStore.php @@ -2,85 +2,53 @@ namespace Kelunik\AcmeClient\Stores; -use Amp\CoroutineResult; +use Amp\File; use Amp\File\FilesystemException; -use InvalidArgumentException; -use Kelunik\Acme\KeyPair; +use Amp\Promise; +use Kelunik\Acme\Crypto\PrivateKey; +use function Amp\call; class KeyStore { private $root; - public function __construct($root = "") { - if (!is_string($root)) { - throw new InvalidArgumentException(sprintf("\$root must be of type string, %s given.", gettype($root))); - } - - $this->root = rtrim(str_replace("\\", "/", $root), "/"); + public function __construct(string $root = '') { + $this->root = rtrim(str_replace("\\", '/', $root), '/'); } - public function get($path) { - if (!is_string($path)) { - throw new InvalidArgumentException(sprintf("\$root must be of type string, %s given.", gettype($path))); - } + public function get(string $path): Promise { + return call(function () use ($path) { + $file = $this->root . '/' . $path; + $privateKey = yield File\get($file); - return \Amp\resolve($this->doGet($path)); - } + // Check key here to be valid, PrivateKey doesn't do that, we fail early here + $res = openssl_pkey_get_private($privateKey); - private function doGet($path) { - if (!is_string($path)) { - throw new InvalidArgumentException(sprintf("\$root must be of type string, %s given.", gettype($path))); - } - - $file = $this->root . "/" . $path; - $realpath = realpath($file); - - if (!$realpath) { - throw new KeyStoreException("File not found: '{$file}'"); - } - - $privateKey = (yield \Amp\File\get($realpath)); - $res = openssl_pkey_get_private($privateKey); - - if ($res === false) { - throw new KeyStoreException("Invalid private key: '{$file}'"); - } - - $publicKey = openssl_pkey_get_details($res)["key"]; - - yield new CoroutineResult(new KeyPair($privateKey, $publicKey)); - } - - public function put($path, KeyPair $keyPair) { - if (!is_string($path)) { - throw new InvalidArgumentException(sprintf("\$root must be of type string, %s given.", gettype($path))); - } - - return \Amp\resolve($this->doPut($path, $keyPair)); - } - - private function doPut($path, KeyPair $keyPair) { - if (!is_string($path)) { - throw new InvalidArgumentException(sprintf("\$root must be of type string, %s given.", gettype($path))); - } - - $file = $this->root . "/" . $path; - - try { - // TODO: Replace with async version once available - if (!file_exists(dirname($file))) { - $success = mkdir(dirname($file), 0755, true); - - if (!$success) { - throw new KeyStoreException("Could not create key store directory."); - } + if ($res === false) { + throw new KeyStoreException("Invalid private key: '{$file}'"); } - yield \Amp\File\put($file, $keyPair->getPrivate()); - yield \Amp\File\chmod($file, 0600); - } catch (FilesystemException $e) { - throw new KeyStoreException("Could not save key.", 0, $e); - } + return new PrivateKey($privateKey); + }); + } - yield new CoroutineResult($keyPair); + public function put(string $path, PrivateKey $key): Promise { + return call(function () use ($path, $key) { + $file = $this->root . '/' . $path; + + try { + $dir = \dirname($file); + + if (!yield File\isdir($dir) && !yield File\mkdir($dir, 0644, true) && !yield File\isdir($dir)) { + throw new FilesystemException("Couldn't create key directory: '{$path}'"); + } + + yield File\put($file, $key->toPem()); + yield File\chmod($file, 0600); + } catch (FilesystemException $e) { + throw new KeyStoreException('Could not save key: ' . $e->getMessage(), 0, $e); + } + + return $key; + }); } } \ No newline at end of file diff --git a/src/Stores/KeyStoreException.php b/src/Stores/KeyStoreException.php index 999cf29..7b32bb6 100644 --- a/src/Stores/KeyStoreException.php +++ b/src/Stores/KeyStoreException.php @@ -2,8 +2,5 @@ namespace Kelunik\AcmeClient\Stores; -use RuntimeException; - -class KeyStoreException extends RuntimeException { - +class KeyStoreException extends \Exception { } \ No newline at end of file diff --git a/src/functions.php b/src/functions.php index b0c6c74..feb54df 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,12 +2,30 @@ namespace Kelunik\AcmeClient; +use Amp\Sync\LocalSemaphore; +use Amp\Sync\Lock; use InvalidArgumentException; use Kelunik\Acme\AcmeException; use Phar; use Symfony\Component\Yaml\Exception\ParseException; use Symfony\Component\Yaml\Yaml; -use Webmozart\Assert\Assert; +use function Amp\call; +use function Amp\coroutine; + +function concurrentMap(int $concurrency, array $values, callable $functor): array { + $semaphore = new LocalSemaphore($concurrency); + + return \array_map(coroutine(function ($value, $key) use ($semaphore, $functor) { + /** @var Lock $lock */ + $lock = yield $semaphore->acquire(); + + try { + return yield call($functor, $value, $key); + } finally { + $lock->release(); + } + }), $values, array_keys($values)); +} /** * Suggests a command based on similarity in a list of available commands. @@ -15,15 +33,13 @@ use Webmozart\Assert\Assert; * @param string $badCommand invalid command * @param array $commands list of available commands * @param int $suggestThreshold similarity threshold + * * @return string suggestion or empty string if no command is similar enough */ -function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) { - Assert::string($badCommand, "Bad command must be a string. Got: %s"); - Assert::integer($suggestThreshold, "Suggest threshold must be an integer. Got: %s"); - +function suggestCommand(string $badCommand, array $commands, int $suggestThreshold = 70): string { $badCommand = strtolower($badCommand); - $bestMatch = ""; + $bestMatch = ''; $bestMatchPercentage = 0; $byRefPercentage = 0; @@ -36,7 +52,7 @@ function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) { } } - return $bestMatchPercentage >= $suggestThreshold ? $bestMatch : ""; + return $bestMatchPercentage >= $suggestThreshold ? $bestMatch : ''; } /** @@ -44,46 +60,46 @@ function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) { * protocol is passed, it will default to HTTPS. * * @param string $uri URI to resolve + * * @return string resolved URI */ -function resolveServer($uri) { - Assert::string($uri, "URI must be a string. Got: %s"); - +function resolveServer(string $uri): string { $shortcuts = [ - "letsencrypt" => "https://acme-v01.api.letsencrypt.org/directory", - "letsencrypt:production" => "https://acme-v01.api.letsencrypt.org/directory", - "letsencrypt:staging" => "https://acme-staging.api.letsencrypt.org/directory", + 'letsencrypt' => 'https://acme-v01.api.letsencrypt.org/directory', + 'letsencrypt:production' => 'https://acme-v01.api.letsencrypt.org/directory', + 'letsencrypt:staging' => 'https://acme-staging.api.letsencrypt.org/directory', ]; if (isset($shortcuts[$uri])) { return $shortcuts[$uri]; } - if (strpos($uri, "/") === false) { - throw new InvalidArgumentException("Invalid server URI: " . $uri); + if (strpos($uri, '/') === false) { + throw new InvalidArgumentException('Invalid server URI: ' . $uri); } - $protocol = substr($uri, 0, strpos($uri, "://")); + $protocol = substr($uri, 0, strpos($uri, '://')); if (!$protocol || $protocol === $uri) { return "https://{$uri}"; - } else { - return $uri; } + + return $uri; } /** * Transforms a directory URI to a valid filename for usage as key file name. * * @param string $server URI to the directory + * * @return string identifier usable as file name */ -function serverToKeyname($server) { - $server = substr($server, strpos($server, "://") + 3); +function serverToKeyname(string $server): string { + $server = substr($server, strpos($server, '://') + 3); - $keyFile = str_replace("/", ".", $server); - $keyFile = preg_replace("@[^a-z0-9._-]@", "", $keyFile); - $keyFile = preg_replace("@\\.+@", ".", $keyFile); + $keyFile = str_replace('/', '.', $server); + $keyFile = preg_replace('@[^a-z0-9._-]@', '', $keyFile); + $keyFile = preg_replace("@\\.+@", '.', $keyFile); return $keyFile; } @@ -93,22 +109,23 @@ function serverToKeyname($server) { * * @return bool {@code true} if running as Phar, {@code false} otherwise */ -function isPhar() { - if (!class_exists("Phar")) { +function isPhar(): bool { + if (!class_exists('Phar')) { return false; } - return Phar::running(true) !== ""; + return Phar::running() !== ''; } /** * Normalizes a path. Replaces all backslashes with slashes and removes trailing slashes. * * @param string $path path to normalize + * * @return string normalized path */ -function normalizePath($path) { - return rtrim(str_replace("\\", "/", $path), "/"); +function normalizePath(string $path): string { + return rtrim(str_replace("\\", '/', $path), '/'); } /** @@ -117,14 +134,14 @@ function normalizePath($path) { * @return string|null Resolves to the config path or null. */ function getConfigPath() { - $paths = isPhar() ? [substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml"] : []; + $paths = isPhar() ? [\substr(\dirname(Phar::running()), \strlen('phar://')) . '/acme-client.yml'] : []; - if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') { - if ($home = getenv("HOME")) { - $paths[] = $home . "/.acme-client.yml"; + if (0 !== stripos(PHP_OS, 'WIN')) { + if ($home = getenv('HOME')) { + $paths[] = $home . '/.acme-client.yml'; } - $paths[] = "/etc/acme-client.yml"; + $paths[] = '/etc/acme-client.yml'; } do { @@ -133,7 +150,7 @@ function getConfigPath() { if (file_exists($path)) { return $path; } - } while (count($paths)); + } while (\count($paths)); return null; } @@ -142,11 +159,12 @@ function getConfigPath() { * Returns a consistent argument description for CLIMate. Valid arguments are "server" and "storage". * * @param string $argument argument name + * * @return array CLIMate argument description * @throws AcmeException if the provided acme-client.yml file is invalid * @throws ConfigException if the provided configuration file is invalid */ -function getArgumentDescription($argument) { +function getArgumentDescription($argument): array { $config = []; if ($configPath = getConfigPath()) { @@ -155,11 +173,11 @@ function getArgumentDescription($argument) { try { $config = Yaml::parse($configContent); - if (isset($config["server"]) && !is_string($config["server"])) { + if (isset($config['server']) && !\is_string($config['server'])) { throw new ConfigException("'server' set, but not a string."); } - if (isset($config["storage"]) && !is_string($config["storage"])) { + if (isset($config['storage']) && !\is_string($config['storage'])) { throw new ConfigException("'storage' set, but not a string."); } } catch (ParseException $e) { @@ -168,41 +186,41 @@ function getArgumentDescription($argument) { } switch ($argument) { - case "server": - $argument = [ - "prefix" => "s", - "longPrefix" => "server", - "description" => "ACME server to use for registration and issuance of certificates.", - "required" => true, + case 'server': + $desc = [ + 'prefix' => 's', + 'longPrefix' => 'server', + 'description' => 'ACME server to use for registration and issuance of certificates.', + 'required' => true, ]; - if (isset($config["server"])) { - $argument["required"] = false; - $argument["defaultValue"] = $config["server"]; + if (isset($config['server'])) { + $desc['required'] = false; + $desc['defaultValue'] = $config['server']; } - return $argument; + return $desc; - case "storage": + case 'storage': $isPhar = isPhar(); - $argument = [ - "longPrefix" => "storage", - "description" => "Storage directory for account keys and certificates.", - "required" => $isPhar, + $desc = [ + 'longPrefix' => 'storage', + 'description' => 'Storage directory for account keys and certificates.', + 'required' => $isPhar, ]; if (!$isPhar) { - $argument["defaultValue"] = dirname(__DIR__) . "/data"; - } else if (isset($config["storage"])) { - $argument["required"] = false; - $argument["defaultValue"] = $config["storage"]; + $desc['defaultValue'] = \dirname(__DIR__) . '/data'; + } else if (isset($config['storage'])) { + $desc['required'] = false; + $desc['defaultValue'] = $config['storage']; } - return $argument; + return $desc; default: - throw new InvalidArgumentException("Unknown argument: " . $argument); + throw new InvalidArgumentException('Unknown argument: ' . $argument); } } @@ -211,27 +229,27 @@ function getArgumentDescription($argument) { * * @return string binary callable, shortened based on PATH and CWD */ -function getBinary() { - $binary = "bin/acme"; +function getBinary(): string { + $binary = 'bin/acme'; if (isPhar()) { - $binary = substr(Phar::running(true), strlen("phar://")); + $binary = substr(Phar::running(), \strlen('phar://')); - $path = getenv("PATH"); + $path = getenv('PATH'); $locations = explode(PATH_SEPARATOR, $path); - $binaryPath = dirname($binary); + $binaryPath = \dirname($binary); foreach ($locations as $location) { if ($location === $binaryPath) { - return substr($binary, strlen($binaryPath) + 1); + return substr($binary, \strlen($binaryPath) + 1); } } $cwd = getcwd(); if ($cwd && strpos($binary, $cwd) === 0) { - $binary = "." . substr($binary, strlen($cwd)); + $binary = '.' . substr($binary, \strlen($cwd)); } } @@ -244,18 +262,19 @@ function getBinary() { * @param string $text text to shorten * @param int $max maximum length * @param string $append appendix when too long + * * @return string shortened string */ -function ellipsis($text, $max = 70, $append = "…") { - if (strlen($text) <= $max) { +function ellipsis($text, $max = 70, $append = '…'): string { + if (\strlen($text) <= $max) { return $text; } $out = substr($text, 0, $max); - if (strpos($text, " ") === false) { + if (strpos($text, ' ') === false) { return $out . $append; } - return preg_replace("/\\w+$/", "", $out) . $append; + return preg_replace("/\\w+$/", '', $out) . $append; } \ No newline at end of file diff --git a/test/FunctionsTest.php b/test/FunctionsTest.php index 4013eea..d86e219 100644 --- a/test/FunctionsTest.php +++ b/test/FunctionsTest.php @@ -2,18 +2,20 @@ namespace Kelunik\AcmeClient; -class FunctionsTest extends \PHPUnit_Framework_TestCase { +use PHPUnit\Framework\TestCase; + +class FunctionsTest extends TestCase { public function testResolveServer() { - $this->assertSame("https://acme-v01.api.letsencrypt.org/directory", resolveServer("letsencrypt")); - $this->assertSame("https://acme-v01.api.letsencrypt.org/directory", resolveServer("letsencrypt:production")); - $this->assertSame("https://acme-staging.api.letsencrypt.org/directory", resolveServer("letsencrypt:staging")); - $this->assertSame("https://acme-v01.api.letsencrypt.org/directory", resolveServer("acme-v01.api.letsencrypt.org/directory")); - $this->assertSame("https://acme-v01.api.letsencrypt.org/directory", resolveServer("https://acme-v01.api.letsencrypt.org/directory")); + $this->assertSame('https://acme-v01.api.letsencrypt.org/directory', resolveServer('letsencrypt')); + $this->assertSame('https://acme-v01.api.letsencrypt.org/directory', resolveServer('letsencrypt:production')); + $this->assertSame('https://acme-staging.api.letsencrypt.org/directory', resolveServer('letsencrypt:staging')); + $this->assertSame('https://acme-v01.api.letsencrypt.org/directory', resolveServer('acme-v01.api.letsencrypt.org/directory')); + $this->assertSame('https://acme-v01.api.letsencrypt.org/directory', resolveServer('https://acme-v01.api.letsencrypt.org/directory')); } public function testSuggestCommand() { - $this->assertSame("acme", suggestCommand("acme!", ["acme"])); - $this->assertSame("", suggestCommand("issue", ["acme"])); + $this->assertSame('acme', suggestCommand('acme!', ['acme'])); + $this->assertSame('', suggestCommand('issue', ['acme'])); } public function testIsPhar() { @@ -21,9 +23,9 @@ class FunctionsTest extends \PHPUnit_Framework_TestCase { } public function testNormalizePath() { - $this->assertSame("/etc/foobar", normalizePath("/etc/foobar")); - $this->assertSame("/etc/foobar", normalizePath("/etc/foobar/")); - $this->assertSame("/etc/foobar", normalizePath("/etc/foobar/")); - $this->assertSame("C:/etc/foobar", normalizePath("C:\\etc\\foobar\\")); + $this->assertSame('/etc/foobar', normalizePath('/etc/foobar')); + $this->assertSame('/etc/foobar', normalizePath('/etc/foobar/')); + $this->assertSame('/etc/foobar', normalizePath('/etc/foobar/')); + $this->assertSame('C:/etc/foobar', normalizePath("C:\\etc\\foobar\\")); } } \ No newline at end of file