10 Commits

Author SHA1 Message Date
Niklas Keller
c02e758a21 Merge pull request #26 from kelunik/automation
Basic working 'auto' command
2016-06-07 09:44:27 +02:00
Niklas Keller
e1ea62b5e7 Doc typo and format fixes 2016-06-07 09:42:52 +02:00
Niklas Keller
a090e99a19 Update documentation for new auto command 2016-06-04 21:47:08 +02:00
Niklas Keller
de3b82da1d Fix output variables for external process outputs 2016-06-04 20:46:57 +02:00
Niklas Keller
791b250742 Show renewals in auto command, fix reference notice, correct exit code for renewed certs 2016-06-04 20:41:48 +02:00
Niklas Keller
9f849691c2 Fix config load path 2016-06-04 19:51:02 +02:00
Niklas Keller
c4d15e2e26 Better exit codes and error messages for auto command 2016-06-04 19:25:46 +02:00
Niklas Keller
0722e104d4 Update deps, use email in setup from config if present 2016-06-03 23:46:38 +02:00
Niklas Keller
583318fa0b Scan for config in /etc and ~, improve automation command 2016-06-03 19:38:47 +02:00
Niklas Keller
3472bd1b3c Basic working 'auto' command 2016-06-03 18:16:29 +02:00
8 changed files with 505 additions and 117 deletions

View File

@@ -40,6 +40,7 @@ HELP;
require __DIR__ . "/../vendor/autoload.php";
$commands = [
"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.",

57
composer.lock generated
View File

@@ -4,8 +4,8 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "c27b6e29c15798ac2cf219d7993e5771",
"content-hash": "a0b266ee9280981bcacbfac39d0969eb",
"hash": "f645228f022f95d362e3cfa543321bd7",
"content-hash": "af2e73ef42c235311d53fcf4eb5aa5ab",
"packages": [
{
"name": "amphp/amp",
@@ -1577,16 +1577,16 @@
},
{
"name": "phpunit/php-code-coverage",
"version": "3.3.3",
"version": "4.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5"
"reference": "900370c81280cc0d942ffbc5912d80464eaee7e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/44cd8e3930e431658d1a5de7d282d5cb37837fd5",
"reference": "44cd8e3930e431658d1a5de7d282d5cb37837fd5",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/900370c81280cc0d942ffbc5912d80464eaee7e9",
"reference": "900370c81280cc0d942ffbc5912d80464eaee7e9",
"shasum": ""
},
"require": {
@@ -1600,7 +1600,7 @@
},
"require-dev": {
"ext-xdebug": ">=2.1.4",
"phpunit/phpunit": "~5"
"phpunit/phpunit": "^5.4"
},
"suggest": {
"ext-dom": "*",
@@ -1610,7 +1610,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3.x-dev"
"dev-master": "4.0.x-dev"
}
},
"autoload": {
@@ -1636,7 +1636,7 @@
"testing",
"xunit"
],
"time": "2016-05-27 16:24:29"
"time": "2016-06-03 05:03:56"
},
{
"name": "phpunit/php-file-iterator",
@@ -1821,16 +1821,16 @@
},
{
"name": "phpunit/phpunit",
"version": "5.3.4",
"version": "5.4.2",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "00dd95ffb48805503817ced06399017df315fe5c"
"reference": "f5726a0262e5f74f8e9cf03128798b64160c441d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/00dd95ffb48805503817ced06399017df315fe5c",
"reference": "00dd95ffb48805503817ced06399017df315fe5c",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f5726a0262e5f74f8e9cf03128798b64160c441d",
"reference": "f5726a0262e5f74f8e9cf03128798b64160c441d",
"shasum": ""
},
"require": {
@@ -1842,11 +1842,11 @@
"myclabs/deep-copy": "~1.3",
"php": "^5.6 || ^7.0",
"phpspec/prophecy": "^1.3.1",
"phpunit/php-code-coverage": "^3.3.0",
"phpunit/php-code-coverage": "^4.0",
"phpunit/php-file-iterator": "~1.4",
"phpunit/php-text-template": "~1.2",
"phpunit/php-timer": "^1.0.6",
"phpunit/phpunit-mock-objects": "^3.1",
"phpunit/phpunit-mock-objects": "^3.2",
"sebastian/comparator": "~1.1",
"sebastian/diff": "~1.2",
"sebastian/environment": "~1.3",
@@ -1866,7 +1866,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.3.x-dev"
"dev-master": "5.4.x-dev"
}
},
"autoload": {
@@ -1892,30 +1892,33 @@
"testing",
"xunit"
],
"time": "2016-05-11 13:28:45"
"time": "2016-06-03 09:59:50"
},
{
"name": "phpunit/phpunit-mock-objects",
"version": "3.1.3",
"version": "3.2.1",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git",
"reference": "151c96874bff6fe61a25039df60e776613a61489"
"reference": "0dc8fd8e87e0366c22b6c25d1f43c4e2e66847b3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/151c96874bff6fe61a25039df60e776613a61489",
"reference": "151c96874bff6fe61a25039df60e776613a61489",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/0dc8fd8e87e0366c22b6c25d1f43c4e2e66847b3",
"reference": "0dc8fd8e87e0366c22b6c25d1f43c4e2e66847b3",
"shasum": ""
},
"require": {
"doctrine/instantiator": "^1.0.2",
"php": ">=5.6",
"phpunit/php-text-template": "~1.2",
"sebastian/exporter": "~1.2"
"php": "^5.6 || ^7.0",
"phpunit/php-text-template": "^1.2",
"sebastian/exporter": "^1.2"
},
"conflict": {
"phpunit/phpunit": "<5.4.0"
},
"require-dev": {
"phpunit/phpunit": "~5"
"phpunit/phpunit": "^5.4"
},
"suggest": {
"ext-soap": "*"
@@ -1923,7 +1926,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.1.x-dev"
"dev-master": "3.2.x-dev"
}
},
"autoload": {
@@ -1948,7 +1951,7 @@
"mock",
"xunit"
],
"time": "2016-04-20 14:39:26"
"time": "2016-06-04 05:52:19"
},
{
"name": "rych/bytesize",

70
doc/advanced-usage.md Normal file
View File

@@ -0,0 +1,70 @@
## Advanced Usage
Please read the document about [basic usage](./usage.md) first.
## Register an Account
```
acme-client setup --email me@example.com
```
After a successful registration you're able to issue certificates.
This client assumes you have a HTTP server setup and running.
You must have a document root setup in order to use this client.
## Issue a Certificate
```
acme-client issue -d example.com:www.example.com -p /var/www/example.com
```
You can separate multiple domains (`-d`) with `,`, `:` or `;`. You can separate multiple document roots (`-p`) with your system's path separator:
* Colon (`:`) for Unix
* Semicolon (`;`) for Windows
If you specify less paths than domains, the last one will be used for the remaining domains.
Please note that Let's Encrypt has rate limits. Currently it's five certificates per domain per seven days. If you combine multiple subdomains in a single certificate, they count as just one certificate. If you just want to test things out, you can use their staging server, which has way higher rate limits by appending `--s letsencrypt:staging`.
## Revoke a Certificate
To revoke a certificate, you need a valid account key, just like for issuance.
```
acme-client revoke --name example.com
```
`--name` is the common name of the certificate that you want to revoke.
## Renew a Certificate
For renewal, there's the `acme-client check` subcommand.
It exists with a non-zero exit code, if the certificate is going to expire soon.
Default check time is 30 days, but you can use `--ttl` to customize it.
You may use this as daily cron:
```
acme-client check --name example.com || acme-client issue ...
```
You can also use a more advanced script to automatically reload the server as well. For this example we assume you're using Nginx.
Something similar should work for Apache. But usually you shouldn't need any script, see [basic usage](./usage.md).
```bash
#!/usr/bin/env bash
acme-client check --name example.com --ttl 30
if [ $? -eq 1 ]; then
acme-client issue -d example.com:www.example.com -p /var/www
if [ $? -eq 0 ]; then
nginx -t -q
if [ $? -eq 0 ]; then
nginx -s reload
fi
fi
fi
```

View File

@@ -1,79 +1,95 @@
# Usage
# Basic Usage
**The client stores all data in `./data` if you're using the Composer installation method, otherwise in the directory you configured. Be sure to backup this folder regularly. It contains your account keys, domain keys and certificates.**
The client stores your account keys, domain keys and certificates in a single directory. If you're using the PHAR,
you usually configure the storage in the configuration file. If you're using it with Composer, all data is stored in `./data`.
Before you can issue certificates, you have to register an account first and read and understand the terms of service of the ACME CA you're using.
For the Let's Encrypt certificate authority, there's a [subscriber agreement](https://letsencrypt.org/repository/) you have to accept.
**Be sure to backup that directory regularly.**
By using this client you agree to any agreement and any further updates by continued usage.
You're responsible to react to updates and stop the automation if you no longer agree with the terms of service.
Before you can issue certificates, you have to register an account. You have to read and understand the terms of service
of the certificate authority you're using. For the Let's Encrypt certificate authority, there's a
[subscriber agreement](https://letsencrypt.org/repository/) you have to accept.
These usage instructions assume you have installed the client globally as a Phar. If you are using the Phar, but don't have it globally, replace `acme-client` with the location to your Phar.
By using this client you agree to any agreement and any further updates by continued usage. You're responsible to react
to updates and stop the automation if you no longer agree with the terms of service.
If you're using the client with Composer, replace `acme-client` with `bin/acme`. You have to specify the server with `-s` / `--server`, because there's currently no config file support for this installation method.
These usage instructions assume you have installed the client globally as a PHAR. If you are using the PHAR,
but don't have it globally, replace `acme-client` with the location to your PHAR or add that path to your `$PATH` variable.
## Register an Account
## Configuration
```
acme-client setup --email me@example.com
The client can be configured using a (global) configuration file. The client takes the first available of
`./acme-client.yml` (if running as PHAR), `$HOME/.acme-client.yml`, `/etc/acme-client.yml` (if not on Windows).
The configuration file has the following format:
```yml
# Storage directory for certificates and keys.
storage: /etc/acme
# Server to use. URL to the ACME directory.
# "letsencrypt" and "letsencrypt:staging" are valid shortcuts.
server: letsencrypt
# E-mail to use for the setup.
# This e-mail will receive expiration notices from Let's Encrypt.
email: me@example.com
# List of certificates to issue.
certificates:
# For each certificate, there are a few options.
#
# Required: paths
# Optional: bits, user
#
# paths: Map of document roots to domains.
# /tmp is used here for domains without a real document root.
# The client will place a file into $path/.well-known/acme-challenge/
# to verify ownership to the CA
#
# bits: Number of bits for the domain private key
#
# user: User running the web server. Challenge files are world readable,
# but some servers might require to be owner of files they serve.
#
- bits: 4096
paths:
/tmp:
- docs.example.org
- git.example.org
# You can have multiple certificate with different users and key options.
- user: www-data
paths:
/var/www: example.org
```
After a successful registration you're able to issue certificates.
This client assumes you have a HTTP server setup and running.
You must have a document root setup in order to use this client.
All configuration keys are optional and can be passed as arguments directly (except for `certificates` when using `acme-client auto`).
## Issue a Certificate
## Certificate Issuance
```
acme-client issue -d example.com:www.example.com -p /var/www/example.com
```
You can use `acme-client auto` to issue certificates and renew them if necessary. It uses the configuration file to
determine the certificates to request. It will store certificates in the configured storage in a sub directory called `./certs`.
You can separate multiple domains (`-d`) with `,`, `:` or `;`. You can separate multiple document roots (`-p`) with your system's path separator:
* Colon (`:`) for Unix
* Semicolon (`;`) for Windows
If everything has been successful, you'll see a message for each issued certificate. If nothing has to be renewed,
the script will be quiet to be cron friendly. If an error occurs, the script will dump all available information.
If you specify less paths than domains, the last one will be used for the remaining domains.
Please note that Let's Encrypt has rate limits. Currently it's five certificates per domain per seven days. If you combine multiple subdomains in a single certificate, they count as just one certificate. If you just want to test things out, you can use their staging server, which has way higher rate limits by appending `--s letsencrypt:staging`.
## Revoke a Certificate
To revoke a certificate, you need a valid account key, just like for issuance.
```
acme-client revoke --name example.com
```
`--name` is the common name of the certificate that you want to revoke.
## Renewing a Certificate
For renewal, there's the `acme-client check` subcommand.
It exists with a non-zero exit code, if the certificate is going to expire soon.
Default check time is 30 days, but you can use `--ttl` to customize it.
You may use this as daily cron:
```
acme-client check --name example.com || acme-client issue ...
```
You can also use a more advanced script to automatically reload the server as well. For this example we assume you're using Nginx. Something similar should work for Apache.
You should execute `acme-client auto` as a daily cron. It's recommended to setup e-mail notifications for all output of
that script.
```bash
#!/usr/bin/env bash
acme-client check --name example.com --ttl 30
if [ $? -eq 1 ]; then
acme-client issue -d example.com:www.example.com -p /var/www
if [ $? -eq 0 ]; then
nginx -t -q
if [ $? -eq 0 ]; then
nginx -s reload
fi
fi
fi
0 0 * * * acme-client auto; exit=$?; if [[ $exit = 4 ]] || [[ $exit = 5 ]]; then service nginx reload; fi
```
| Exit Code | Description |
|-----------|-------------|
| 0 | Nothing to do, all certificates still valid. |
| 1 | Config file invalid. |
| 2 | Issue during account setup. |
| 3 | Error during issuance. |
| 4 | Error during issuance, but some certificates could be renewed. |
| 5 | Everything fine, new certificates have been issued. |
Exit codes `4` and `5` usually need a server reload, to reload the new certificates. It's already handled in the recommended
cron setup.
If you want a more fine grained control or revoke certificates, you can have a look at the [advanced usage](./advanced-usage.md) document. The client allows to handle setup / issuance / revocation and other commands
separately from `acme-client auto`.

263
src/Commands/Auto.php Normal file
View File

@@ -0,0 +1,263 @@
<?php
namespace Kelunik\AcmeClient\Commands;
use Amp\CoroutineResult;
use Amp\File\FilesystemException;
use Amp\Process;
use Kelunik\Acme\AcmeException;
use League\CLImate\Argument\Manager;
use League\CLImate\CLImate;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
class Auto implements Command {
const EXIT_CONFIG_ERROR = 1;
const EXIT_SETUP_ERROR = 2;
const EXIT_ISSUANCE_ERROR = 3;
const EXIT_ISSUANCE_PARTIAL = 4;
const EXIT_ISSUANCE_OK = 5;
const STATUS_NO_CHANGE = 0;
const STATUS_RENEWED = 1;
private $climate;
public function __construct(CLImate $climate) {
$this->climate = $climate;
}
public function execute(Manager $args) {
return \Amp\resolve($this->doExecute($args));
}
/**
* @param Manager $args
* @return \Generator
*/
private function doExecute(Manager $args) {
$server = $args->get("server");
$storage = $args->get("storage");
$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 (!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;
}
$command = implode(" ", array_map("escapeshellarg", [
PHP_BINARY,
$GLOBALS["argv"][0],
"setup",
"--server",
$server,
"--storage",
$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;
}
$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));
}
list($chunkErrors, $chunkValues) = (yield \Amp\any($promises));
$errors += $chunkErrors;
$values += $chunkValues;
}
$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.");
}
}
}
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}");
}
$exitCode = $status["renewed"] > 0
? self::EXIT_ISSUANCE_PARTIAL
: self::EXIT_ISSUANCE_ERROR;
yield new CoroutineResult($exitCode);
return;
}
if ($status["renewed"] > 0) {
yield new CoroutineResult(self::EXIT_ISSUANCE_OK);
return;
}
}
/**
* @param array $certificate certificate configuration
* @param string $server server to use for issuance
* @param string $storage storage directory
* @return \Generator
* @throws AcmeException if something does wrong
*/
private function checkAndIssue(array $certificate, $server, $storage) {
$domainPathMap = $this->toDomainPathMap($certificate["paths"]);
$domains = array_keys($domainPathMap);
$commonName = reset($domains);
$args = [
PHP_BINARY,
$GLOBALS["argv"][0],
"check",
"--server",
$server,
"--storage",
$storage,
"--name",
$commonName,
];
$command = implode(" ", array_map("escapeshellarg", $args));
$process = new Process($command);
$result = (yield $process->exec(Process::BUFFER_ALL));
if ($result->exit === 0) {
// No need for renewal
yield new CoroutineResult(self::STATUS_NO_CHANGE);
return;
}
if ($result->exit === 1) {
// Renew certificate
$args = [
PHP_BINARY,
$GLOBALS["argv"][0],
"issue",
"--server",
$server,
"--storage",
$storage,
"--domains",
implode(",", $domains),
"--path",
implode(PATH_SEPARATOR, array_values($domainPathMap)),
];
if (isset($certificate["user"])) {
$args[] = "--user";
$args[] = $certificate["user"];
}
if (isset($certificate["bits"])) {
$args[] = "--bits";
$args[] = $certificate["bits"];
}
$command = implode(" ", array_map("escapeshellarg", $args));
$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);
}
yield new CoroutineResult(self::STATUS_RENEWED);
return;
}
throw new AcmeException("Unexpected exit code ({$result->exit}) for '{$command}'." . PHP_EOL . $result->stdout . PHP_EOL . PHP_EOL . $result->stderr);
}
private function toDomainPathMap(array $paths) {
$result = [];
foreach ($paths as $path => $domains) {
$domains = (array) $domains;
foreach ($domains as $domain) {
if (isset($result[$domain])) {
throw new \LogicException("Duplicate domain: {$domain}");
}
$result[$domain] = $path;
}
}
return $result;
}
public static function getDefinition() {
$args = [
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
"config" => [
"prefix" => "c",
"longPrefix" => "config",
"description" => "Configuration file to read.",
"required" => true,
],
];
$configPath = \Kelunik\AcmeClient\getConfigPath();
if ($configPath) {
$args["config"]["required"] = false;
$args["config"]["defaultValue"] = $configPath;
}
return $args;
}
}

View File

@@ -14,6 +14,7 @@ use Kelunik\AcmeClient\Stores\KeyStore;
use Kelunik\AcmeClient\Stores\KeyStoreException;
use League\CLImate\Argument\Manager;
use League\CLImate\CLImate;
use Symfony\Component\Yaml\Yaml;
class Setup implements Command {
private $climate;
@@ -85,9 +86,7 @@ class Setup implements Command {
}
public static function getDefinition() {
return [
$args = [
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
"email" => [
@@ -96,5 +95,18 @@ class Setup implements Command {
"required" => true,
],
];
$configPath = \Kelunik\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"];
}
}
return $args;
}
}

5
src/ConfigException.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
namespace Kelunik\AcmeClient;
class ConfigException extends \Exception { }

View File

@@ -111,43 +111,59 @@ function normalizePath($path) {
return rtrim(str_replace("\\", "/", $path), "/");
}
/**
* Gets the most appropriate config path to use.
*
* @return string|null Resolves to the config path or null.
*/
function getConfigPath() {
$paths = isPhar() ? [substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml"] : [];
if (strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
if ($home = getenv("HOME")) {
$paths[] = $home . "/.acme-client.yml";
}
$paths[] = "/etc/acme-client.yml";
}
do {
$path = array_shift($paths);
if (file_exists($path)) {
return $path;
}
} while (count($paths));
return null;
}
/**
* 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) {
$isPhar = \Kelunik\AcmeClient\isPhar();
$config = [];
if ($isPhar) {
$configPath = substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml";
if ($configPath = getConfigPath()) {
$configContent = file_get_contents($configPath);
if (file_exists($configPath)) {
$configContent = file_get_contents($configPath);
try {
$config = Yaml::parse($configContent);
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());
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"])) {
throw new ConfigException("'storage' set, but not a string.");
}
} catch (ParseException $e) {
throw new AcmeException("Unable to parse the configuration ({$configPath}): " . $e->getMessage());
}
}
@@ -168,6 +184,8 @@ function getArgumentDescription($argument) {
return $argument;
case "storage":
$isPhar = isPhar();
$argument = [
"longPrefix" => "storage",
"description" => "Storage directory for account keys and certificates.",