Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a33ac65a77 | ||
|
|
746bf84adb | ||
|
|
ff6e14e2de | ||
|
|
9f917ec3ec | ||
|
|
5381587bbf | ||
|
|
266cc06746 | ||
|
|
dd34937e96 | ||
|
|
d34b6eb09d | ||
|
|
1c4a2387e9 | ||
|
|
8549ff9e46 | ||
|
|
93c94d1a9b | ||
|
|
6173b779e1 | ||
|
|
e8f35811fb | ||
|
|
c36ced9b7c | ||
|
|
9ea97f18e1 | ||
|
|
1a06a5cadf | ||
|
|
3e6be4abf1 | ||
|
|
67e4fab090 | ||
|
|
5e0d126834 | ||
|
|
e47eb9c636 |
15
.php_cs
Normal file
15
.php_cs
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
return Symfony\CS\Config\Config::create()
|
||||
->level(Symfony\CS\FixerInterface::NONE_LEVEL)
|
||||
->fixers([
|
||||
"psr2",
|
||||
"-braces",
|
||||
"-psr0",
|
||||
])
|
||||
->finder(
|
||||
Symfony\CS\Finder\DefaultFinder::create()
|
||||
->in(__DIR__ . "/bin")
|
||||
->in(__DIR__ . "/src")
|
||||
->in(__DIR__ . "/test")
|
||||
);
|
||||
28
.travis.yml
Normal file
28
.travis.yml
Normal file
@@ -0,0 +1,28 @@
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 5.5
|
||||
- 5.6
|
||||
- 7.0
|
||||
- nightly
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- vendor
|
||||
|
||||
install:
|
||||
- phpenv config-rm xdebug.ini
|
||||
- composer self-update
|
||||
- composer config --global discard-changes true
|
||||
- if [[ ${TRAVIS_PHP_VERSION:0:3} == "5.5" ]]; then composer require --dev --no-update phpunit/phpunit ^4; fi
|
||||
- composer require satooshi/php-coveralls dev-master --dev --no-update
|
||||
- composer update --ignore-platform-reqs
|
||||
- composer show --installed
|
||||
|
||||
script:
|
||||
- find -name "*.php" -not -path "./vendor/*" -print0 | xargs -n 1 -0 php -l
|
||||
- $(php -r 'if (PHP_MAJOR_VERSION >= 7) echo "phpdbg -qrr"; else echo "php";') vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
|
||||
- php vendor/bin/php-cs-fixer --diff --dry-run -v fix
|
||||
|
||||
after_script:
|
||||
- php vendor/bin/coveralls -v
|
||||
54
README.md
54
README.md
@@ -15,7 +15,35 @@ It's an alternative for the [official client](https://github.com/letsencrypt/let
|
||||
* PHP 5.5+
|
||||
* Composer
|
||||
|
||||
**Instructions**
|
||||
**Instructions using the Phar**
|
||||
|
||||
```bash
|
||||
# Go to https://github.com/kelunik/acme-client/releases/latest
|
||||
# Download the latest release archive
|
||||
|
||||
# Run it.
|
||||
chmod +x acme-client.phar
|
||||
./acme-client.phar
|
||||
|
||||
# Or install it globally
|
||||
mv ./acme-client.phar /usr/local/bin/acme-client
|
||||
```
|
||||
|
||||
If you want to update, just replace the old phar with a new one.
|
||||
|
||||
All commands require an additional `--storage` argument when using the phar. That's the path where your keys and certificates will be stored.
|
||||
On Unix you could use something like `--storage /etc/acme`.
|
||||
|
||||
If you're using the phar, you can add a file called `acme-client.yml` next to it with the two keys `storage` and `server`.
|
||||
These values will be used as default if you don't specify them, but you can still use another server by explicitly adding it as argument.
|
||||
|
||||
```yml
|
||||
# Sample YAML configuration:
|
||||
storage: /etc/acme
|
||||
server: letsencrypt
|
||||
```
|
||||
|
||||
**Instructions using Composer**
|
||||
|
||||
```bash
|
||||
# Clone repository
|
||||
@@ -99,7 +127,7 @@ bin/acme issue -s letsencrypt -d example.com:www.example.com -p /var/www/example
|
||||
To revoke a certificate, you need a valid account key currently, just like for issuance.
|
||||
|
||||
```
|
||||
bin/acme revoke --name example.com
|
||||
bin/acme revoke --name example.com -s letsencrypt
|
||||
```
|
||||
|
||||
For renewal, there's the `bin/acme check` subcommand.
|
||||
@@ -110,4 +138,26 @@ You may use this as daily cron:
|
||||
|
||||
```
|
||||
bin/acme check --name example.com --ttl 30 -s letsencrypt || bin/acme issue ...
|
||||
```
|
||||
|
||||
You can also use a more advanced script to automatically reload the server as well.
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd /git/kelunik/acme-client
|
||||
|
||||
bin/acme check --name example.com --ttl 30 -s letsencrypt
|
||||
|
||||
if [ $? -eq 1 ]; then
|
||||
bin/acme issue -d example.com:www.example.com -p /var/www -s letsencrypt
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
nginx -t -q
|
||||
|
||||
if [ $? -eq 0 ]; then
|
||||
nginx -s reload
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
```
|
||||
44
bin/acme
44
bin/acme
@@ -2,12 +2,7 @@
|
||||
<?php
|
||||
|
||||
use Auryn\Injector;
|
||||
use Bramus\Monolog\Formatter\ColoredLineFormatter;
|
||||
use Kelunik\AcmeClient\LoggerColorScheme;
|
||||
use League\CLImate\CLImate;
|
||||
use Monolog\Handler\StreamHandler;
|
||||
use Monolog\Logger;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
if (!file_exists(__DIR__ . "/../vendor/autoload.php")) {
|
||||
echo <<<HELP
|
||||
@@ -35,7 +30,7 @@ $commands = [
|
||||
"revoke",
|
||||
];
|
||||
|
||||
$help = implode("\n ", array_map(function($command) {
|
||||
$help = implode("\n ", array_map(function ($command) {
|
||||
return "bin/acme {$command}";
|
||||
}, $commands));
|
||||
|
||||
@@ -65,19 +60,21 @@ if (!in_array(PHP_SAPI, ["cli", "phpdbg"], true)) {
|
||||
}
|
||||
|
||||
if (count($argv) === 1 || in_array($argv[1], ["h", "-h", "help", "--help"], true)) {
|
||||
print $help;
|
||||
$climate->out($help);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!in_array($argv[1], $commands)) {
|
||||
$climate->error("Unknown command '{$argv[1]}'. Use --help for a list of available commands.");
|
||||
$climate->br()->error(" Unknown command '{$argv[1]}'. Use --help for a list of available commands.");
|
||||
|
||||
$suggestion = \Kelunik\AcmeClient\suggestCommand($argv[1], $commands);
|
||||
|
||||
if ($suggestion) {
|
||||
$climate->br()->info(" Did you mean '$suggestion'?")->br();
|
||||
$climate->br()->out(" Did you mean '$suggestion'?");
|
||||
}
|
||||
|
||||
$climate->br();
|
||||
|
||||
exit(1);
|
||||
}
|
||||
|
||||
@@ -95,26 +92,25 @@ try {
|
||||
if (count($argv) === 3 && in_array($argv[2], ["h", "-h", "--help", "help"], true)) {
|
||||
$climate->usage(["bin/acme {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
exit(0);
|
||||
} else {
|
||||
$climate->usage(["bin/acme {$argv[1]}"]);
|
||||
$climate->br();
|
||||
|
||||
$climate->error($e->getMessage());
|
||||
$climate->br();
|
||||
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
$handler = new StreamHandler("php://stdout", Logger::DEBUG);
|
||||
$handler->setFormatter(new ColoredLineFormatter(new LoggerColorScheme, null, null, true, true));
|
||||
|
||||
$logger = new Logger("ACME");
|
||||
$logger->pushHandler($handler);
|
||||
|
||||
$injector->alias(LoggerInterface::class, Logger::class);
|
||||
$injector->share($logger);
|
||||
$injector->share($climate);
|
||||
|
||||
$command = $injector->make($class);
|
||||
|
||||
Amp\run(function () use ($command, $climate, $logger) {
|
||||
$handler = function($e) use ($logger) {
|
||||
Amp\run(function () use ($command, $climate) {
|
||||
$handler = function ($e) use ($climate) {
|
||||
$error = (string) $e;
|
||||
$lines = explode("\n", $error);
|
||||
$lines = array_filter($lines, function ($line) {
|
||||
@@ -122,14 +118,20 @@ Amp\run(function () use ($command, $climate, $logger) {
|
||||
});
|
||||
|
||||
foreach ($lines as $line) {
|
||||
$logger->error($line);
|
||||
$climate->error($line)->br();
|
||||
}
|
||||
|
||||
exit(1);
|
||||
};
|
||||
|
||||
try {
|
||||
yield $command->execute($climate->arguments);
|
||||
$exitCode = (yield $command->execute($climate->arguments));
|
||||
|
||||
if ($exitCode === null) {
|
||||
$logger->warning("Invalid exit code: null, falling back to 0. Please consider reporting this as bug.");
|
||||
}
|
||||
|
||||
exit($exitCode);
|
||||
} catch (Throwable $e) {
|
||||
$handler($e);
|
||||
} catch (Exception $e) {
|
||||
|
||||
@@ -11,17 +11,20 @@
|
||||
"tls"
|
||||
],
|
||||
"require": {
|
||||
"amphp/process": "^0.1.1",
|
||||
"bramus/monolog-colored-line-formatter": "^2",
|
||||
"php": "^5.5|^7",
|
||||
"ext-openssl": "*",
|
||||
"amphp/process": "^0.1.1",
|
||||
"kelunik/acme": "^0.3",
|
||||
"kelunik/certificate": "^1",
|
||||
"league/climate": "^3",
|
||||
"monolog/monolog": "^1.17",
|
||||
"php": "^5.5|^7",
|
||||
"psr/log": "^1",
|
||||
"rdlowrey/auryn": "^1",
|
||||
"webmozart/assert": "^1"
|
||||
"webmozart/assert": "^1",
|
||||
"symfony/yaml": "^3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5",
|
||||
"fabpot/php-cs-fixer": "^1.9",
|
||||
"macfja/phar-builder": "dev-master#a2db582eab26ef7b15144c013408749a79fae361"
|
||||
},
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
@@ -40,16 +43,12 @@
|
||||
"src/functions.php"
|
||||
]
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5",
|
||||
"macfja/phar-builder": "^0.2.3"
|
||||
},
|
||||
"extra": {
|
||||
"phar-builder": {
|
||||
"compression": "GZip",
|
||||
"name": "acme.phar",
|
||||
"name": "acme-client.phar",
|
||||
"output-dir": "build",
|
||||
"include": ["bin", "src", "vendor"],
|
||||
"include": ["src", "vendor/kelunik/acme/res"],
|
||||
"entry-point": "bin/acme"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,16 +2,18 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\CoroutineResult;
|
||||
use Kelunik\AcmeClient\Stores\CertificateStore;
|
||||
use Kelunik\AcmeClient\Stores\CertificateStoreException;
|
||||
use Kelunik\Certificate\Certificate;
|
||||
use League\CLImate\Argument\Manager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use League\CLImate\CLImate;
|
||||
|
||||
class Check implements Command {
|
||||
private $logger;
|
||||
private $climate;
|
||||
|
||||
public function __construct(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
public function __construct(CLImate $climate) {
|
||||
$this->climate = $climate;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -26,31 +28,37 @@ class Check implements Command {
|
||||
$server = \Kelunik\AcmeClient\resolveServer($args->get("server"));
|
||||
$server = \Kelunik\AcmeClient\serverToKeyname($server);
|
||||
|
||||
$path = dirname(dirname(__DIR__)) . "/data/certs/" . $server;
|
||||
$path = \Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $server;
|
||||
$certificateStore = new CertificateStore($path);
|
||||
|
||||
$pem = (yield $certificateStore->get($args->get("name")));
|
||||
$cert = new Certificate($pem);
|
||||
try {
|
||||
$pem = (yield $certificateStore->get($args->get("name")));
|
||||
} catch (CertificateStoreException $e) {
|
||||
$this->climate->br()->error(" Certificate not found.")->br();
|
||||
|
||||
$this->logger->info("Certificate is valid until " . date("d.m.Y", $cert->getValidTo()));
|
||||
|
||||
if ($cert->getValidTo() > time() + $args->get("ttl") * 24 * 60 * 60) {
|
||||
exit(0);
|
||||
yield new CoroutineResult(1);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->warning("Certificate is going to expire within the specified " . $args->get("ttl") . " days.");
|
||||
$cert = new Certificate($pem);
|
||||
|
||||
exit(1);
|
||||
$this->climate->br();
|
||||
$this->climate->whisper(" Certificate is valid until " . date("d.m.Y", $cert->getValidTo()))->br();
|
||||
|
||||
if ($cert->getValidTo() > time() + $args->get("ttl") * 24 * 60 * 60) {
|
||||
yield new CoroutineResult(0);
|
||||
return;
|
||||
}
|
||||
|
||||
$this->climate->comment(" Certificate is going to expire within the specified " . $args->get("ttl") . " days.")->br();
|
||||
|
||||
yield new CoroutineResult(1);
|
||||
}
|
||||
|
||||
public static function getDefinition() {
|
||||
return [
|
||||
"server" => [
|
||||
"prefix" => "s",
|
||||
"longPrefix" => "server",
|
||||
"description" => "",
|
||||
"required" => true,
|
||||
],
|
||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||
"name" => [
|
||||
"longPrefix" => "name",
|
||||
"description" => "Common name of the certificate to check.",
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\CombinatorException;
|
||||
use Amp\CoroutineResult;
|
||||
use Amp\Dns\Record;
|
||||
use Exception;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
@@ -15,15 +15,15 @@ use Kelunik\AcmeClient\Stores\ChallengeStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStoreException;
|
||||
use League\CLImate\Argument\Manager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use League\CLImate\CLImate;
|
||||
use stdClass;
|
||||
use Throwable;
|
||||
|
||||
class Issue implements Command {
|
||||
private $logger;
|
||||
private $climate;
|
||||
|
||||
public function __construct(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
public function __construct(CLImate $climate) {
|
||||
$this->climate = $climate;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -45,11 +45,11 @@ class Issue implements Command {
|
||||
}
|
||||
}
|
||||
|
||||
$domains = array_map("trim", explode(":", str_replace(",", ":", $args->get("domains"))));
|
||||
$domains = array_map("trim", explode(":", str_replace([",", ";"], ":", $args->get("domains"))));
|
||||
yield \Amp\resolve($this->checkDnsRecords($domains));
|
||||
|
||||
$docRoots = explode(":", str_replace("\\", "/", $args->get("path")));
|
||||
$docRoots = array_map(function($root) {
|
||||
$docRoots = explode(PATH_SEPARATOR, str_replace("\\", "/", $args->get("path")));
|
||||
$docRoots = array_map(function ($root) {
|
||||
return rtrim($root, "/");
|
||||
}, $docRoots);
|
||||
|
||||
@@ -64,7 +64,7 @@ class Issue implements Command {
|
||||
);
|
||||
}
|
||||
|
||||
$keyStore = new KeyStore(dirname(dirname(__DIR__)) . "/data");
|
||||
$keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get("storage")));
|
||||
|
||||
$server = \Kelunik\AcmeClient\resolveServer($args->get("server"));
|
||||
$keyFile = \Kelunik\AcmeClient\serverToKeyname($server);
|
||||
@@ -72,13 +72,12 @@ class Issue implements Command {
|
||||
try {
|
||||
$keyPair = (yield $keyStore->get("accounts/{$keyFile}.pem"));
|
||||
} catch (KeyStoreException $e) {
|
||||
$this->logger->error("Account key not found, did you run 'bin/acme setup'?");
|
||||
|
||||
exit(1);
|
||||
throw new AcmeException("Account key not found, did you run 'bin/acme setup'?", 0, $e);
|
||||
}
|
||||
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair));
|
||||
$this->climate->br();
|
||||
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair));
|
||||
$promises = [];
|
||||
|
||||
foreach ($domains as $i => $domain) {
|
||||
@@ -89,7 +88,7 @@ class Issue implements Command {
|
||||
|
||||
if (!empty($errors)) {
|
||||
foreach ($errors as $error) {
|
||||
$this->logger->error($error->getMessage());
|
||||
$this->climate->error($error->getMessage());
|
||||
}
|
||||
|
||||
throw new AcmeException("Issuance failed, not all challenges could be solved.");
|
||||
@@ -105,16 +104,21 @@ class Issue implements Command {
|
||||
$keyPair = (yield $keyStore->put($path, $keyPair));
|
||||
}
|
||||
|
||||
$this->logger->info("Requesting certificate ...");
|
||||
$this->climate->br();
|
||||
$this->climate->whisper(" Requesting certificate ...");
|
||||
|
||||
$location = (yield $acme->requestCertificate($keyPair, $domains));
|
||||
$certificates = (yield $acme->pollForCertificate($location));
|
||||
|
||||
$path = dirname(dirname(__DIR__)) . "/data/certs/" . $keyFile;
|
||||
$path = \Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $keyFile;
|
||||
$certificateStore = new CertificateStore($path);
|
||||
yield $certificateStore->put($certificates);
|
||||
|
||||
$this->logger->info("Successfully issued certificate, see {$path}/" . reset($domains));
|
||||
$this->climate->info(" Successfully issued certificate.");
|
||||
$this->climate->info(" See {$path}/" . reset($domains));
|
||||
$this->climate->br();
|
||||
|
||||
yield new CoroutineResult(0);
|
||||
}
|
||||
|
||||
private function solveChallenge(AcmeService $acme, KeyPair $keyPair, $domain, $path) {
|
||||
@@ -132,11 +136,9 @@ class Issue implements Command {
|
||||
throw new AcmeException("Protocol violation: Invalid Token!");
|
||||
}
|
||||
|
||||
$this->logger->debug("Generating payload...");
|
||||
$payload = $acme->generateHttp01Payload($keyPair, $token);
|
||||
|
||||
$this->logger->info("Providing payload at http://{$domain}/.well-known/acme-challenge/{$token}");
|
||||
|
||||
$this->climate->whisper(" Providing payload at http://{$domain}/.well-known/acme-challenge/{$token}");
|
||||
|
||||
$challengeStore = new ChallengeStore($path);
|
||||
|
||||
@@ -144,13 +146,10 @@ class Issue implements Command {
|
||||
$challengeStore->put($token, $payload, isset($user) ? $user : null);
|
||||
|
||||
yield $acme->verifyHttp01Challenge($domain, $token, $payload);
|
||||
$this->logger->info("Successfully self-verified challenge.");
|
||||
|
||||
yield $acme->answerChallenge($challenge->uri, $payload);
|
||||
$this->logger->info("Answered challenge... waiting");
|
||||
|
||||
yield $acme->pollForChallenge($location);
|
||||
$this->logger->info("Challenge successful. {$domain} is now authorized.");
|
||||
|
||||
$this->climate->comment(" {$domain} is now authorized.");
|
||||
|
||||
yield $challengeStore->delete($token);
|
||||
} catch (Exception $e) {
|
||||
@@ -179,8 +178,6 @@ class Issue implements Command {
|
||||
if (!empty($errors)) {
|
||||
throw new AcmeException("Couldn't resolve the following domains to an IPv4 record: " . implode(array_keys($errors)));
|
||||
}
|
||||
|
||||
$this->logger->info("Checked DNS records, all fine.");
|
||||
}
|
||||
|
||||
private function findSuitableCombination(stdClass $response) {
|
||||
@@ -205,22 +202,18 @@ class Issue implements Command {
|
||||
|
||||
public static function getDefinition() {
|
||||
return [
|
||||
"server" => [
|
||||
"prefix" => "s",
|
||||
"longPrefix" => "server",
|
||||
"description" => "Server to use for issuance, see also 'bin/acme setup'.",
|
||||
"required" => true,
|
||||
],
|
||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||
"domains" => [
|
||||
"prefix" => "d",
|
||||
"longPrefix" => "domains",
|
||||
"description" => "Colon separated list of domains to request a certificate for.",
|
||||
"description" => "Colon / Semicolon / Comma separated list of domains to request a certificate for.",
|
||||
"required" => true,
|
||||
],
|
||||
"path" => [
|
||||
"prefix" => "p",
|
||||
"longPrefix" => "path",
|
||||
"description" => "Colon 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.",
|
||||
"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" => [
|
||||
|
||||
@@ -2,23 +2,21 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\CoroutineResult;
|
||||
use Amp\File\FilesystemException;
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Kelunik\Acme\AcmeService;
|
||||
use Kelunik\AcmeClient\Stores\CertificateStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
use Kelunik\Certificate\Certificate;
|
||||
use League\CLImate\Argument\Manager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use League\CLImate\CLImate;
|
||||
|
||||
class Revoke implements Command {
|
||||
private $logger;
|
||||
private $climate;
|
||||
|
||||
public function __construct(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
public function __construct(CLImate $climate) {
|
||||
$this->climate = $climate;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -26,7 +24,7 @@ class Revoke implements Command {
|
||||
}
|
||||
|
||||
private function doExecute(Manager $args) {
|
||||
$keyStore = new KeyStore(dirname(dirname(__DIR__)) . "/data");
|
||||
$keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get("storage")));
|
||||
|
||||
$server = \Kelunik\AcmeClient\resolveServer($args->get("server"));
|
||||
$keyFile = \Kelunik\AcmeClient\serverToKeyname($server);
|
||||
@@ -34,9 +32,10 @@ class Revoke implements Command {
|
||||
$keyPair = (yield $keyStore->get("accounts/{$keyFile}.pem"));
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair);
|
||||
|
||||
$this->logger->info("Revoking certificate ...");
|
||||
$this->climate->br();
|
||||
$this->climate->whisper(" Revoking certificate ...");
|
||||
|
||||
$path = dirname(dirname(__DIR__)) . "/data/certs/" . $keyFile . "/" . $args->get("name") . "/cert.pem";
|
||||
$path = \Kelunik\AcmeClient\normalizePath($args->get("storage")) . "/certs/" . $keyFile . "/" . $args->get("name") . "/cert.pem";
|
||||
|
||||
try {
|
||||
$pem = (yield \Amp\File\get($path));
|
||||
@@ -46,24 +45,27 @@ class Revoke implements Command {
|
||||
}
|
||||
|
||||
if ($cert->getValidTo() < time()) {
|
||||
$this->logger->warning("Certificate did already expire, no need to revoke it.");
|
||||
$this->climate->comment(" Certificate did already expire, no need to revoke it.");
|
||||
}
|
||||
|
||||
$this->logger->info("Certificate was valid for: " . implode(", ", $cert->getNames()));
|
||||
yield $acme->revokeCertificate($pem);
|
||||
$this->logger->info("Certificate has been revoked.");
|
||||
$names = $cert->getNames();
|
||||
$this->climate->whisper(" Certificate was valid for " . count($names) . " domains.");
|
||||
$this->climate->whisper(" - " . implode(PHP_EOL . " - ", $names) . PHP_EOL);
|
||||
|
||||
yield (new CertificateStore(dirname(dirname(__DIR__)) . "/data/certs/" . $keyFile))->delete($args->get("name"));
|
||||
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() {
|
||||
return [
|
||||
"server" => [
|
||||
"prefix" => "s",
|
||||
"longPrefix" => "server",
|
||||
"description" => "",
|
||||
"required" => true,
|
||||
],
|
||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||
"name" => [
|
||||
"longPrefix" => "name",
|
||||
"description" => "Common name of the certificate to be revoked.",
|
||||
|
||||
@@ -2,10 +2,9 @@
|
||||
|
||||
namespace Kelunik\AcmeClient\Commands;
|
||||
|
||||
use Amp\CoroutineResult;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Dns\ResolutionException;
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use Kelunik\Acme\AcmeClient;
|
||||
use Kelunik\Acme\AcmeException;
|
||||
@@ -15,14 +14,13 @@ use Kelunik\Acme\Registration;
|
||||
use Kelunik\AcmeClient\Stores\KeyStore;
|
||||
use Kelunik\AcmeClient\Stores\KeyStoreException;
|
||||
use League\CLImate\Argument\Manager;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use RuntimeException;
|
||||
use League\CLImate\CLImate;
|
||||
|
||||
class Setup implements Command {
|
||||
private $logger;
|
||||
private $climate;
|
||||
|
||||
public function __construct(LoggerInterface $logger) {
|
||||
$this->logger = $logger;
|
||||
public function __construct(CLImate $climate) {
|
||||
$this->climate = $climate;
|
||||
}
|
||||
|
||||
public function execute(Manager $args) {
|
||||
@@ -39,29 +37,32 @@ class Setup implements Command {
|
||||
$path = "accounts/{$keyFile}.pem";
|
||||
$bits = 4096;
|
||||
|
||||
$keyStore = new KeyStore(dirname(dirname(__DIR__)) . "/data");
|
||||
$keyStore = new KeyStore(\Kelunik\AcmeClient\normalizePath($args->get("storage")));
|
||||
|
||||
$this->climate->br();
|
||||
|
||||
try {
|
||||
$this->logger->info("Loading private key ...");
|
||||
$keyPair = (yield $keyStore->get($path));
|
||||
$this->logger->info("Existing private key successfully loaded.");
|
||||
$this->climate->whisper(" Using existing private key ...");
|
||||
} catch (KeyStoreException $e) {
|
||||
$this->logger->info("No existing private key found, generating new one ...");
|
||||
$keyPair = (new OpenSSLKeyGenerator)->generate($bits);
|
||||
$this->logger->info("Generated new private key with {$bits} bits.");
|
||||
$this->climate->whisper(" No private key found, generating new one ...");
|
||||
|
||||
$this->logger->info("Saving new private key ...");
|
||||
$keyPair = (new OpenSSLKeyGenerator)->generate($bits);
|
||||
$keyPair = (yield $keyStore->put($path, $keyPair));
|
||||
$this->logger->info("New private key successfully saved.");
|
||||
|
||||
$this->climate->whisper(" Generated new private key with {$bits} bits.");
|
||||
}
|
||||
|
||||
$acme = new AcmeService(new AcmeClient($server, $keyPair), $keyPair);
|
||||
|
||||
$this->logger->info("Registering with ACME server " . substr($server, 8) . " ...");
|
||||
$this->climate->whisper(" Registering with " . substr($server, 8) . " ...");
|
||||
|
||||
/** @var Registration $registration */
|
||||
$registration = (yield $acme->register($email));
|
||||
$this->logger->notice("Registration successful with the following contact information: " . implode(", ", $registration->getContact()));
|
||||
$this->climate->info(" Registration successful. Contacts: " . implode(", ", $registration->getContact()));
|
||||
$this->climate->br();
|
||||
|
||||
yield new CoroutineResult(0);
|
||||
}
|
||||
|
||||
private function checkEmail($email) {
|
||||
@@ -83,16 +84,14 @@ class Setup implements Command {
|
||||
}
|
||||
|
||||
public static function getDefinition() {
|
||||
|
||||
|
||||
return [
|
||||
"server" => [
|
||||
"prefix" => "s",
|
||||
"longPrefix" => "server",
|
||||
"description" => "ACME server to use for registration and issuance of certificates.",
|
||||
"required" => true,
|
||||
],
|
||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||
"email" => [
|
||||
"longPrefix" => "email",
|
||||
"description" => "Email for important issues, will be sent to the ACME server.",
|
||||
"description" => "E-mail for important issues, will be sent to the ACME server.",
|
||||
"required" => true,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient;
|
||||
|
||||
use Bramus\Ansi\ControlSequences\EscapeSequences\Enums\SGR;
|
||||
use Bramus\Monolog\Formatter\ColorSchemes\ColorSchemeInterface;
|
||||
use Bramus\Monolog\Formatter\ColorSchemes\ColorSchemeTrait;
|
||||
use Monolog\Logger;
|
||||
|
||||
class LoggerColorScheme implements ColorSchemeInterface {
|
||||
use ColorSchemeTrait {
|
||||
ColorSchemeTrait::__construct as private __constructTrait;
|
||||
}
|
||||
|
||||
public function __construct() {
|
||||
$this->__constructTrait();
|
||||
|
||||
$this->setColorizeArray([
|
||||
Logger::DEBUG => $this->ansi->color(SGR::COLOR_FG_WHITE)->get(),
|
||||
Logger::INFO => $this->ansi->color(SGR::COLOR_FG_WHITE_BRIGHT)->get(),
|
||||
Logger::NOTICE => $this->ansi->color(SGR::COLOR_FG_GREEN)->get(),
|
||||
Logger::WARNING => $this->ansi->color(SGR::COLOR_FG_YELLOW)->get(),
|
||||
Logger::ERROR => $this->ansi->color(SGR::COLOR_FG_RED)->get(),
|
||||
Logger::CRITICAL => $this->ansi->color(SGR::COLOR_FG_RED)->get(),
|
||||
Logger::ALERT => $this->ansi->color(SGR::COLOR_FG_RED)->get(),
|
||||
Logger::EMERGENCY => $this->ansi->color(SGR::COLOR_FG_RED)->get(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
namespace Kelunik\AcmeClient;
|
||||
|
||||
use Kelunik\Acme\AcmeException;
|
||||
use Phar;
|
||||
use Symfony\Component\Yaml\Exception\ParseException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
function suggestCommand($badCommand, array $commands, $suggestThreshold = 70) {
|
||||
@@ -56,4 +60,86 @@ function serverToKeyname($server) {
|
||||
$keyFile = preg_replace("@\\.+@", ".", $keyFile);
|
||||
|
||||
return $keyFile;
|
||||
}
|
||||
|
||||
function isPhar() {
|
||||
if (!class_exists("Phar")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Phar::running(true) !== "";
|
||||
}
|
||||
|
||||
function normalizePath($path) {
|
||||
return rtrim(str_replace("\\", "/", $path), "/");
|
||||
}
|
||||
|
||||
function getArgumentDescription($argument) {
|
||||
$isPhar = \Kelunik\AcmeClient\isPhar();
|
||||
|
||||
$config = [];
|
||||
|
||||
if ($isPhar) {
|
||||
$configPath = substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml";
|
||||
|
||||
if (file_exists($configPath)) {
|
||||
$configContent = file_get_contents($configPath);
|
||||
|
||||
try {
|
||||
$value = Yaml::parse($configContent);
|
||||
|
||||
if (isset($value["server"]) && is_string($value["server"])) {
|
||||
$config["server"] = $value["server"];
|
||||
unset($value["server"]);
|
||||
}
|
||||
|
||||
if (isset($value["storage"]) && is_string($value["storage"])) {
|
||||
$config["storage"] = $value["storage"];
|
||||
unset($value["storage"]);
|
||||
}
|
||||
|
||||
if (!empty($value)) {
|
||||
throw new AcmeException("Provided YAML file had unknown options: " . implode(", ", array_keys($value)));
|
||||
}
|
||||
} catch (ParseException $e) {
|
||||
throw new AcmeException("Unable to parse the YAML file ({$configPath}): " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch ($argument) {
|
||||
case "server":
|
||||
$argument = [
|
||||
"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"];
|
||||
}
|
||||
|
||||
return $argument;
|
||||
|
||||
case "storage":
|
||||
$argument = [
|
||||
"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"];
|
||||
}
|
||||
|
||||
return $argument;
|
||||
|
||||
default:
|
||||
throw new \InvalidArgumentException("Unknown argument: " . $argument);
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,15 @@ class FunctionsTest extends \PHPUnit_Framework_TestCase {
|
||||
$this->assertSame("acme", suggestCommand("acme!", ["acme"]));
|
||||
$this->assertSame("", suggestCommand("issue", ["acme"]));
|
||||
}
|
||||
|
||||
public function testIsPhar() {
|
||||
$this->assertFalse(isPhar());
|
||||
}
|
||||
|
||||
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\\"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user