Scan for config in /etc and ~, improve automation command
This commit is contained in:
@@ -8,6 +8,7 @@ use Amp\Process;
|
|||||||
use Kelunik\Acme\AcmeException;
|
use Kelunik\Acme\AcmeException;
|
||||||
use League\CLImate\Argument\Manager;
|
use League\CLImate\Argument\Manager;
|
||||||
use League\CLImate\CLImate;
|
use League\CLImate\CLImate;
|
||||||
|
use Symfony\Component\Yaml\Exception\ParseException;
|
||||||
use Symfony\Component\Yaml\Yaml;
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
class Auto implements Command {
|
class Auto implements Command {
|
||||||
@@ -38,6 +39,10 @@ class Auto implements Command {
|
|||||||
$this->climate->error("Config file ({$configPath}) not found.");
|
$this->climate->error("Config file ({$configPath}) not found.");
|
||||||
yield new CoroutineResult(1);
|
yield new CoroutineResult(1);
|
||||||
return;
|
return;
|
||||||
|
} catch (ParseException $e) {
|
||||||
|
$this->climate->error("Config file ({$configPath}) had an invalid format and couldn't be parsed.");
|
||||||
|
yield new CoroutineResult(1);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($config["email"])) {
|
if (!isset($config["email"])) {
|
||||||
@@ -46,20 +51,12 @@ class Auto implements Command {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isset($config["certificates"])) {
|
if (!isset($config["certificates"]) || !is_array($config["certificates"])) {
|
||||||
$this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section.");
|
$this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section that's an array.");
|
||||||
yield new CoroutineResult(2);
|
yield new CoroutineResult(2);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($config["storage"])) {
|
|
||||||
$storage = $config["storage"];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($config["server"])) {
|
|
||||||
$server = $config["server"];
|
|
||||||
}
|
|
||||||
|
|
||||||
$command = implode(" ", array_map("escapeshellarg", [
|
$command = implode(" ", array_map("escapeshellarg", [
|
||||||
PHP_BINARY,
|
PHP_BINARY,
|
||||||
$GLOBALS["argv"][0],
|
$GLOBALS["argv"][0],
|
||||||
@@ -73,27 +70,36 @@ class Auto implements Command {
|
|||||||
]));
|
]));
|
||||||
|
|
||||||
$process = new Process($command);
|
$process = new Process($command);
|
||||||
$result = (yield $process->exec());
|
$result = (yield $process->exec(Process::BUFFER_ALL));
|
||||||
|
|
||||||
if ($result->exit !== 0) {
|
if ($result->exit !== 0) {
|
||||||
$this->climate->error("Registration failed ({$result->exit})");
|
$this->climate->error("Registration failed ({$result->exit})");
|
||||||
$this->climate->error($command);
|
$this->climate->error($command);
|
||||||
|
$this->climate->br()->out($result->out);
|
||||||
|
$this->climate->br()->error($result->err);
|
||||||
yield new CoroutineResult(3);
|
yield new CoroutineResult(3);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$promises = [];
|
$certificateChunks = array_chunk($config["certificates"], 10);
|
||||||
|
|
||||||
foreach ($config["certificates"] as $certificate) {
|
$errors = [];
|
||||||
$promises[] = \Amp\resolve($this->checkAndIssue($certificate, $server, $storage));
|
|
||||||
|
foreach ($certificateChunks as $chunk) {
|
||||||
|
$promises = [];
|
||||||
|
|
||||||
|
foreach ($chunk as $certificate) {
|
||||||
|
$promises[] = \Amp\resolve($this->checkAndIssue($certificate, $server, $storage));
|
||||||
|
}
|
||||||
|
|
||||||
|
list($errors) = (yield \Amp\any($promises));
|
||||||
|
$errors = array_merge($errors, $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
list($errors) = (yield \Amp\any($promises));
|
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
foreach ($errors as $i => $error) {
|
foreach ($errors as $i => $error) {
|
||||||
$certificate = $config["certificates"][$i];
|
$certificate = $config["certificates"][$i];
|
||||||
$this->climate->error("Issuance for the following domains failed: " . implode(", ", array_keys($this->toDomainPathMap((array) $certificate->paths))));
|
$this->climate->error("Issuance for the following domains failed: " . implode(", ", array_keys($this->toDomainPathMap($certificate["paths"]))));
|
||||||
$this->climate->error("Reason: {$error}");
|
$this->climate->error("Reason: {$error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +116,7 @@ class Auto implements Command {
|
|||||||
* @throws AcmeException if something does wrong
|
* @throws AcmeException if something does wrong
|
||||||
*/
|
*/
|
||||||
private function checkAndIssue(array $certificate, $server, $storage) {
|
private function checkAndIssue(array $certificate, $server, $storage) {
|
||||||
$domainPathMap = $this->toDomainPathMap((array) $certificate["paths"]);
|
$domainPathMap = $this->toDomainPathMap($certificate["paths"]);
|
||||||
$commonName = reset(array_keys($domainPathMap));
|
$commonName = reset(array_keys($domainPathMap));
|
||||||
|
|
||||||
$args = [
|
$args = [
|
||||||
@@ -137,7 +143,6 @@ class Auto implements Command {
|
|||||||
|
|
||||||
if ($result->exit === 1) {
|
if ($result->exit === 1) {
|
||||||
// Renew certificate
|
// Renew certificate
|
||||||
|
|
||||||
$args = [
|
$args = [
|
||||||
PHP_BINARY,
|
PHP_BINARY,
|
||||||
$GLOBALS["argv"][0],
|
$GLOBALS["argv"][0],
|
||||||
@@ -146,9 +151,9 @@ class Auto implements Command {
|
|||||||
$server,
|
$server,
|
||||||
"--storage",
|
"--storage",
|
||||||
$storage,
|
$storage,
|
||||||
"-d",
|
"--domains",
|
||||||
implode(",", array_keys($domainPathMap)),
|
implode(",", array_keys($domainPathMap)),
|
||||||
"-p",
|
"--path",
|
||||||
implode(PATH_SEPARATOR, array_values($domainPathMap)),
|
implode(PATH_SEPARATOR, array_values($domainPathMap)),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -180,17 +185,15 @@ class Auto implements Command {
|
|||||||
private function toDomainPathMap(array $paths) {
|
private function toDomainPathMap(array $paths) {
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
foreach ($paths as $pathDomainMap) {
|
foreach ($paths as $path => $domains) {
|
||||||
foreach ($pathDomainMap as $path => $domains) {
|
$domains = (array) $domains;
|
||||||
$domains = (array) $domains;
|
|
||||||
|
|
||||||
foreach ($domains as $domain) {
|
foreach ($domains as $domain) {
|
||||||
if (isset($result[$domain])) {
|
if (isset($result[$domain])) {
|
||||||
throw new \LogicException("Duplicate domain: {$domain}");
|
throw new \LogicException("Duplicate domain: {$domain}");
|
||||||
}
|
|
||||||
|
|
||||||
$result[$domain] = $path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$result[$domain] = $path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -198,7 +201,7 @@ class Auto implements Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function getDefinition() {
|
public static function getDefinition() {
|
||||||
return [
|
$args = [
|
||||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||||
"config" => [
|
"config" => [
|
||||||
@@ -208,5 +211,14 @@ class Auto implements Command {
|
|||||||
"required" => true,
|
"required" => true,
|
||||||
],
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
$configPath = \Kelunik\AcmeClient\getConfigPath();
|
||||||
|
|
||||||
|
if ($configPath) {
|
||||||
|
$args["config"]["required"] = false;
|
||||||
|
$args["config"]["defaultValue"] = $configPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $args;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,8 +85,6 @@ class Setup implements Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static function getDefinition() {
|
public static function getDefinition() {
|
||||||
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
"server" => \Kelunik\AcmeClient\getArgumentDescription("server"),
|
||||||
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
"storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"),
|
||||||
|
|||||||
5
src/ConfigException.php
Normal file
5
src/ConfigException.php
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Kelunik\AcmeClient;
|
||||||
|
|
||||||
|
class ConfigException extends \Exception { }
|
||||||
@@ -111,43 +111,59 @@ function normalizePath($path) {
|
|||||||
return rtrim(str_replace("\\", "/", $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".
|
* Returns a consistent argument description for CLIMate. Valid arguments are "server" and "storage".
|
||||||
*
|
*
|
||||||
* @param string $argument argument name
|
* @param string $argument argument name
|
||||||
* @return array CLIMate argument description
|
* @return array CLIMate argument description
|
||||||
* @throws AcmeException if the provided acme-client.yml file is invalid
|
* @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) {
|
||||||
$isPhar = \Kelunik\AcmeClient\isPhar();
|
|
||||||
|
|
||||||
$config = [];
|
$config = [];
|
||||||
|
|
||||||
if ($isPhar) {
|
if ($configPath = getConfigPath()) {
|
||||||
$configPath = substr(dirname(Phar::running(true)), strlen("phar://")) . "/acme-client.yml";
|
$configContent = file_get_contents($configPath);
|
||||||
|
|
||||||
if (file_exists($configPath)) {
|
try {
|
||||||
$configContent = file_get_contents($configPath);
|
$config = Yaml::parse($configContent);
|
||||||
|
|
||||||
try {
|
if (isset($config["server"]) && !is_string($config["server"])) {
|
||||||
$value = Yaml::parse($configContent);
|
throw new ConfigException("'server' set, but not a string.");
|
||||||
|
|
||||||
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["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;
|
return $argument;
|
||||||
|
|
||||||
case "storage":
|
case "storage":
|
||||||
|
$isPhar = isPhar();
|
||||||
|
|
||||||
$argument = [
|
$argument = [
|
||||||
"longPrefix" => "storage",
|
"longPrefix" => "storage",
|
||||||
"description" => "Storage directory for account keys and certificates.",
|
"description" => "Storage directory for account keys and certificates.",
|
||||||
|
|||||||
Reference in New Issue
Block a user