From 3472bd1b3c3684c06286d6ff36ab05cd89aee672 Mon Sep 17 00:00:00 2001 From: Niklas Keller Date: Mon, 28 Mar 2016 15:40:08 +0200 Subject: [PATCH] Basic working 'auto' command --- bin/acme | 1 + src/Commands/Auto.php | 212 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 src/Commands/Auto.php diff --git a/bin/acme b/bin/acme index d56c3ba..090b22f 100755 --- a/bin/acme +++ b/bin/acme @@ -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.", diff --git a/src/Commands/Auto.php b/src/Commands/Auto.php new file mode 100644 index 0000000..c69d82e --- /dev/null +++ b/src/Commands/Auto.php @@ -0,0 +1,212 @@ +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(1); + return; + } + + if (!isset($config["email"])) { + $this->climate->error("Config file ({$configPath}) didn't have a 'email' set."); + yield new CoroutineResult(2); + return; + } + + if (!isset($config["certificates"])) { + $this->climate->error("Config file ({$configPath}) didn't have a 'certificates' section."); + yield new CoroutineResult(2); + return; + } + + if (isset($config["storage"])) { + $storage = $config["storage"]; + } + + if (isset($config["server"])) { + $server = $config["server"]; + } + + $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()); + + if ($result->exit !== 0) { + $this->climate->error("Registration failed ({$result->exit})"); + $this->climate->error($command); + yield new CoroutineResult(3); + return; + } + + $promises = []; + + foreach ($config["certificates"] as $certificate) { + $promises[] = \Amp\resolve($this->checkAndIssue($certificate, $server, $storage)); + } + + list($errors) = (yield \Amp\any($promises)); + + if (!empty($errors)) { + foreach ($errors as $i => $error) { + $certificate = $config["certificates"][$i]; + $this->climate->error("Issuance for the following domains failed: " . implode(", ", array_keys($this->toDomainPathMap((array) $certificate->paths)))); + $this->climate->error("Reason: {$error}"); + } + + yield new CoroutineResult(3); + 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((array) $certificate["paths"]); + $commonName = reset(array_keys($domainPathMap)); + + $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()); + + if ($result->exit === 0) { + // No need for renewal + return; + } + + if ($result->exit === 1) { + // Renew certificate + + $args = [ + PHP_BINARY, + $GLOBALS["argv"][0], + "issue", + "--server", + $server, + "--storage", + $storage, + "-d", + implode(",", array_keys($domainPathMap)), + "-p", + 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()); + + if ($result->exit !== 0) { + throw new AcmeException("Unexpected exit code ({$result->exit}) for '{$command}'."); + } + + return; + } + + throw new AcmeException("Unexpected exit code ({$result->exit}) for '{$command}'."); + } + + private function toDomainPathMap(array $paths) { + $result = []; + + foreach ($paths as $pathDomainMap) { + foreach ($pathDomainMap 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() { + return [ + "server" => \Kelunik\AcmeClient\getArgumentDescription("server"), + "storage" => \Kelunik\AcmeClient\getArgumentDescription("storage"), + "config" => [ + "prefix" => "c", + "longPrefix" => "config", + "description" => "Configuration file to read.", + "required" => true, + ], + ]; + } +} \ No newline at end of file