20 Commits

Author SHA1 Message Date
Niklas Keller
f4cabf755b Update dependencies, ignore cookies 2017-05-10 10:22:51 +02:00
Niklas Keller
d78b465739 Add link to phar download 2017-03-18 08:52:18 +01:00
Niklas Keller
aa8186471c Issue certificates sequential, allow changing the challenge concurrency 2017-01-30 19:15:33 +01:00
Niklas Keller
888588cf00 Separate script for cron 2017-01-13 09:00:39 +01:00
Niklas Keller
44f218d8c2 Update dependencies 2017-01-05 02:20:47 +01:00
Niklas Keller
af44670353 Improve error message for failed domain DNS 2017-01-02 00:07:23 +01:00
Niklas Keller
0bd3525938 Use 'composer update' instead of 'composer install'
We need another PHPUnit version for PHP 5.5.
2016-12-15 10:53:03 +01:00
Niklas Keller
c3f8424785 Fix tests 2016-12-15 10:35:24 +01:00
Niklas Keller
040aebe993 Improve error message on timed out MX query
Any error, not only NoRecordExceptions, resulted in a MX record not found
error message. The previous message is now only shown if there's really no
record. Otherwise a more generic message is shown now.

Fixes #43.
2016-12-14 19:21:41 +01:00
Niklas Keller
8944aee552 Fall back to global config for server and storage if possible 2016-10-22 12:21:09 +02:00
Niklas Keller
5b2c47c30f Args::exists → Args::defined 2016-10-22 12:15:07 +02:00
Niklas Keller
ecb46af5c0 Pass right variables to checkAndIssue 2016-10-22 12:07:10 +02:00
Niklas Keller
253d3f476b Renew if not all names are covered
Renew a certificate if not all names are covered by the current certificate yet.
Adds a new `--names` option to `check` that makes `check` fail if not all names are covered.
Resolves #34.
2016-10-22 11:41:34 +02:00
Niklas Keller
c6d9c2016c Make 'server' and 'storage' optional for 'auto'
Resolves #35. Takes the value from the global config file as default argument.
If it doesn't exist, but something in the config file exists, it takes that.
If a command line argument is provided, it always takes precedence.
2016-10-22 11:27:52 +02:00
Niklas Keller
b4d4da7a51 Update dependencies 2016-10-22 11:27:25 +02:00
Niklas Keller
0d61689da1 Wait for challenge to be written to disk before continuing
This also makes exceptions visible now. This commit resolves #37.
2016-10-22 11:10:01 +02:00
Niklas Keller
349a12aae6 Merge pull request #41 from spikyjt/patch-1
Correct brackets
2016-10-22 11:01:32 +02:00
JT
28ae97f135 Correct brackets
I'm sure I did this properly the first time. Must have lost concentration!
2016-10-22 09:55:36 +01:00
Niklas Keller
0fc0e45e57 Merge pull request #40 from spikyjt/patch-1
POSIX compliant cron example
2016-10-21 14:15:12 +02:00
JT
9356a060e7 POSIX compliant cron example
Changed the cron auto example to be POSIX compliant and use full paths.
Changed `exit` variable to `RC` (commonly used in system scripts for "return code") as `exit` is a shell builtin.
Added note about setting the full path as $PATH may not be set.
2016-10-21 11:56:29 +01:00
10 changed files with 564 additions and 326 deletions

View File

@@ -11,12 +11,10 @@ cache:
- vendor
install:
- phpenv config-rm xdebug.ini
- composer self-update
- phpenv config-rm xdebug.ini || true
- 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 update
- composer require satooshi/php-coveralls dev-master --dev
- composer show --installed
script:

View File

@@ -135,6 +135,7 @@ try {
$injector = new Injector;
$injector->share($climate);
$injector->share(new AcmeFactory);
$injector->share(new Amp\Artax\Client(new Amp\Artax\Cookie\NullCookieJar));
$command = $injector->make($class);

View File

@@ -22,9 +22,9 @@
"symfony/yaml": "^3.0"
},
"require-dev": {
"phpunit/phpunit": "^5",
"phpunit/phpunit": "^4|^5",
"friendsofphp/php-cs-fixer": "^1.9",
"macfja/phar-builder": "dev-events-dev-files"
"macfja/phar-builder": "^0.2.5"
},
"license": "MIT",
"authors": [

738
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,7 +2,7 @@
## Installation using Phar
This is the preferred installation method for usage on a production system.
This is the preferred installation method for usage on a production system. You can download `acme-client.phar` in the [release section](https://github.com/kelunik/acme-client/releases).
### Requirements

View File

@@ -77,8 +77,25 @@ the script will be quiet to be cron friendly. If an error occurs, the script wil
You should execute `acme-client auto` as a daily cron. It's recommended to setup e-mail notifications for all output of
that script.
Create a new script, e.g. in `/usr/local/bin/acme-renew`. The `PATH` might need to be modified to suit your system.
```bash
0 0 * * * acme-client auto; exit=$?; if [[ $exit = 4 ]] || [[ $exit = 5 ]]; then service nginx reload; fi
#!/usr/bin/env bash
export PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
acme-client auto
RC=$?
if [ $RC = 4 ] || [ $RC = 5 ]; then
service nginx reload
fi
```
```sh
# Cron Job Configuration
0 0 * * * /usr/local/bin/acme-renew
```
| Exit Code | Description |

View File

@@ -37,8 +37,6 @@ class Auto implements Command {
* @return \Generator
*/
private function doExecute(Manager $args) {
$server = $args->get("server");
$storage = $args->get("storage");
$configPath = $args->get("config");
try {
@@ -55,6 +53,30 @@ class Auto implements Command {
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);
@@ -67,14 +89,22 @@ class Auto implements Command {
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",
$server,
$config["server"],
"--storage",
$storage,
$config["storage"],
"--email",
$config["email"],
]));
@@ -91,22 +121,16 @@ class Auto implements Command {
return;
}
$certificateChunks = array_chunk($config["certificates"], 10, true);
$errors = [];
$values = [];
foreach ($certificateChunks as $certificateChunk) {
$promises = [];
foreach ($certificateChunk as $certificate) {
$promises[] = \Amp\resolve($this->checkAndIssue($certificate, $server, $storage));
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;
}
list($chunkErrors, $chunkValues) = (yield \Amp\any($promises));
$errors += $chunkErrors;
$values += $chunkValues;
}
$status = [
@@ -149,10 +173,11 @@ class Auto implements Command {
* @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
*/
private function checkAndIssue(array $certificate, $server, $storage) {
private function checkAndIssue(array $certificate, $server, $storage, $concurrency = null) {
$domainPathMap = $this->toDomainPathMap($certificate["paths"]);
$domains = array_keys($domainPathMap);
$commonName = reset($domains);
@@ -167,6 +192,8 @@ class Auto implements Command {
$storage,
"--name",
$commonName,
"--names",
implode(",", $domains),
];
$command = implode(" ", array_map("escapeshellarg", $args));
@@ -206,6 +233,11 @@ class Auto implements Command {
$args[] = $certificate["bits"];
}
if ($concurrency) {
$args[] = "--challenge-concurrency";
$args[] = $concurrency;
}
$command = implode(" ", array_map("escapeshellarg", $args));
$process = new Process($command);
@@ -277,9 +309,15 @@ MESSAGE;
}
public static function getDefinition() {
$server = \Kelunik\AcmeClient\getArgumentDescription("server");
$storage = \Kelunik\AcmeClient\getArgumentDescription("storage");
$server["required"] = false;
$storage["required"] = false;
$args = [
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
"server" => $server,
"storage" => $storage,
"config" => [
"prefix" => "c",
"longPrefix" => "config",

View File

@@ -45,6 +45,18 @@ class Check implements Command {
$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;
}
}
if ($cert->getValidTo() > time() + $args->get("ttl") * 24 * 60 * 60) {
yield new CoroutineResult(0);
return;
@@ -70,6 +82,11 @@ class Check implements Command {
"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,
],
];
}
}

View File

@@ -82,7 +82,9 @@ class Issue implements Command {
$acme = $this->acmeFactory->build($server, $keyPair);
$errors = [];
$domainChunks = array_chunk($domains, 10, true);
$concurrency = $args->get("challenge-concurrency");
$domainChunks = array_chunk($domains, \min(20, \max($concurrency, 1)), true);
foreach ($domainChunks as $domainChunk) {
$promises = [];
@@ -153,7 +155,7 @@ class Issue implements Command {
$challengeStore = new ChallengeStore($path);
try {
$challengeStore->put($token, $payload, isset($user) ? $user : null);
yield $challengeStore->put($token, $payload, isset($user) ? $user : null);
yield $acme->verifyHttp01Challenge($domain, $token, $payload);
yield $acme->answerChallenge($challenge->uri, $payload);
@@ -194,7 +196,13 @@ class Issue implements Command {
}
if (!empty($errors)) {
throw new AcmeException("Couldn't resolve the following domains to an IPv4 nor IPv6 record: " . implode(", ", array_keys($errors)));
$failedDomains = implode(", ", array_keys($errors));
$reasons = implode("\n\n", array_map(function ($exception) {
/** @var \Exception|\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}");
}
}
@@ -245,6 +253,12 @@ class Issue implements Command {
"defaultValue" => 2048,
"castTo" => "int",
],
"challenge-concurrency" => [
"longPrefix" => "challenge-concurrency",
"description" => "Number of challenges to be solved concurrently.",
"defaultValue" => 10,
"castTo" => "int",
],
];
}
}

View File

@@ -3,6 +3,7 @@
namespace Kelunik\AcmeClient\Commands;
use Amp\CoroutineResult;
use Amp\Dns\NoRecordException;
use Amp\Dns\Record;
use Amp\Dns\ResolutionException;
use InvalidArgumentException;
@@ -80,8 +81,10 @@ class Setup implements Command {
try {
yield \Amp\Dns\query($host, Record::MX);
} catch (ResolutionException $e) {
} catch (NoRecordException $e) {
throw new AcmeException("No MX record defined for '{$host}'");
} catch (ResolutionException $e) {
throw new AcmeException("Dns query for an MX record on '{$host}' failed for the following reason: " . $e->getMessage(), null, $e);
}
}