Major update, add renew command, rename register to setup
This commit is contained in:
82
src/Stores/CertificateStore.php
Normal file
82
src/Stores/CertificateStore.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use Amp\File\FilesystemException;
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
use InvalidArgumentException;
|
||||
use Kelunik\Certificate\Certificate;
|
||||
use function Amp\File\put;
|
||||
use function Amp\File\chmod;
|
||||
use function Amp\File\chown;
|
||||
use function Amp\File\scandir;
|
||||
use function Amp\File\unlink;
|
||||
use function Amp\resolve;
|
||||
use function Amp\File\rmdir;
|
||||
|
||||
class CertificateStore {
|
||||
private $root;
|
||||
|
||||
public function __construct(string $root) {
|
||||
$this->root = rtrim(str_replace("\\", "/", $root), "/");
|
||||
}
|
||||
|
||||
public function put(array $certificates): Promise {
|
||||
return resolve($this->doPut($certificates));
|
||||
}
|
||||
|
||||
private function doPut(array $certificates): Generator {
|
||||
if (empty($certificates)) {
|
||||
throw new InvalidArgumentException("Empty array not allowed");
|
||||
}
|
||||
|
||||
$cert = new Certificate($certificates[0]);
|
||||
$commonName = $cert->getSubject()->getCommonName();
|
||||
|
||||
if (!$commonName) {
|
||||
throw new CertificateStoreException("Certificate doesn't have a common name.");
|
||||
}
|
||||
|
||||
// See https://github.com/amphp/dns/blob/4c4d450d4af26fc55dc56dcf45ec7977373a38bf/lib/functions.php#L83
|
||||
if (isset($commonName[253]) || !preg_match("~^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9]){0,1})(?:\\.[a-z0-9][a-z0-9-]{0,61}[a-z0-9])*$~i", $commonName)) {
|
||||
throw new CertificateStoreException("Invalid common name: '{$commonName}'");
|
||||
}
|
||||
|
||||
try {
|
||||
$chain = array_slice($certificates, 1);
|
||||
$path = $this->root . "/" . $commonName;
|
||||
$realpath = realpath($path);
|
||||
|
||||
if (!$realpath && !mkdir($path, 0770, true)) {
|
||||
throw new FilesystemException("Couldn't create certificate directory: '{$path}'");
|
||||
}
|
||||
|
||||
yield put($path . "/cert.pem", $certificates[0]);
|
||||
yield chown($path . "/cert.pem", 0, 0);
|
||||
yield chmod($path . "/cert.pem", 0640);
|
||||
|
||||
yield put($path . "/fullchain.pem", implode("\n", $certificates));
|
||||
yield chown($path . "/fullchain.pem", 0, 0);
|
||||
yield chmod($path . "/fullchain.pem", 0640);
|
||||
|
||||
yield put($path . "/chain.pem", implode("\n", $chain));
|
||||
yield chown($path . "/chain.pem", 0, 0);
|
||||
yield chmod($path . "/chain.pem", 0640);
|
||||
} catch (FilesystemException $e) {
|
||||
throw new CertificateStoreException("Couldn't save certificates for '{$commonName}'", 0, $e);
|
||||
}
|
||||
}
|
||||
|
||||
public function delete(string $name): Promise {
|
||||
return resolve($this->doDelete($name));
|
||||
}
|
||||
|
||||
private function doDelete(string $name): Generator {
|
||||
foreach ((yield scandir($this->root . "/" . $name)) as $file) {
|
||||
yield unlink($this->root . "/" . $name . "/" . $file);
|
||||
}
|
||||
|
||||
yield rmdir($this->root . "/" . $name);
|
||||
}
|
||||
}
|
||||
9
src/Stores/CertificateStoreException.php
Normal file
9
src/Stores/CertificateStoreException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class CertificateStoreException extends RuntimeException {
|
||||
|
||||
}
|
||||
60
src/Stores/ChallengeStore.php
Normal file
60
src/Stores/ChallengeStore.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
use function Amp\File\put;
|
||||
use function Amp\File\unlink;
|
||||
use function Amp\resolve;
|
||||
|
||||
class ChallengeStore {
|
||||
private $docroot;
|
||||
|
||||
public function __construct(string $docroot) {
|
||||
$this->docroot = rtrim(str_replace("\\", "/", $docroot), "/");
|
||||
}
|
||||
|
||||
public function put(string $token, string $payload, string $user): Promise {
|
||||
return resolve($this->doPut($token, $payload, $user));
|
||||
}
|
||||
|
||||
private function doPut(string $token, string $payload, string $user): Generator {
|
||||
$path = $this->docroot . "/.well-known/acme-challenge";
|
||||
$realpath = realpath($path);
|
||||
|
||||
if (!realpath($this->docroot)) {
|
||||
throw new ChallengeStoreException("Document root doesn't exist: '{$this->docroot}'");
|
||||
}
|
||||
|
||||
if (!$realpath && !@mkdir($path, 0770, true)) {
|
||||
throw new ChallengeStoreException("Couldn't create public directory to serve the challenges: '{$path}'");
|
||||
}
|
||||
|
||||
if (!$userInfo = posix_getpwnam($user)) {
|
||||
throw new ChallengeStoreException("Unknown user: '{$user}'");
|
||||
}
|
||||
|
||||
// TODO: Make async, see https://github.com/amphp/file/issues/6
|
||||
chown($this->docroot . "/.well-known", $userInfo["uid"]);
|
||||
chown($this->docroot . "/.well-known/acme-challenge", $userInfo["uid"]);
|
||||
|
||||
yield put("{$path}/{$token}", $payload);
|
||||
|
||||
chown("{$path}/{$token}", $userInfo["uid"]);
|
||||
chmod("{$path}/{$token}", 0660);
|
||||
}
|
||||
|
||||
public function delete(string $token): Promise {
|
||||
return resolve($this->doDelete($token));
|
||||
}
|
||||
|
||||
private function doDelete(string $token): Generator {
|
||||
$path = $this->docroot . "/.well-known/acme-challenge/{$token}";
|
||||
$realpath = realpath($path);
|
||||
|
||||
if ($realpath) {
|
||||
yield unlink($realpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Stores/ChallengeStoreException.php
Normal file
9
src/Stores/ChallengeStoreException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class ChallengeStoreException extends RuntimeException {
|
||||
|
||||
}
|
||||
66
src/Stores/KeyStore.php
Normal file
66
src/Stores/KeyStore.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use Amp\File\FilesystemException;
|
||||
use Amp\Promise;
|
||||
use Generator;
|
||||
use Kelunik\Acme\KeyPair;
|
||||
use function Amp\File\chmod;
|
||||
use function Amp\File\chown;
|
||||
use function Amp\File\get;
|
||||
use function Amp\File\put;
|
||||
use function Amp\resolve;
|
||||
|
||||
class KeyStore {
|
||||
private $root;
|
||||
|
||||
public function __construct(string $root = "") {
|
||||
$this->root = rtrim(str_replace("\\", "/", $root), "/");
|
||||
}
|
||||
|
||||
public function get(string $path): Promise {
|
||||
return resolve($this->doGet($path));
|
||||
}
|
||||
|
||||
private function doGet(string $path): Generator {
|
||||
$file = $this->root . "/" . $path;
|
||||
$realpath = realpath($file);
|
||||
|
||||
if (!$realpath) {
|
||||
throw new KeyStoreException("File not found: '{$file}'");
|
||||
}
|
||||
|
||||
$privateKey = yield get($realpath);
|
||||
$res = openssl_pkey_get_private($privateKey);
|
||||
|
||||
if ($res === false) {
|
||||
throw new KeyStoreException("Invalid private key: '{$file}'");
|
||||
}
|
||||
|
||||
$publicKey = openssl_pkey_get_details($res)["key"];
|
||||
|
||||
return new KeyPair($privateKey, $publicKey);
|
||||
}
|
||||
|
||||
public function put(string $path, KeyPair $keyPair): Promise {
|
||||
return resolve($this->doPut($path, $keyPair));
|
||||
}
|
||||
|
||||
private function doPut(string $path, KeyPair $keyPair): Generator {
|
||||
$file = $this->root . "/" . $path;
|
||||
|
||||
try {
|
||||
// TODO: Replace with async version once available
|
||||
mkdir(dirname($file), 0770, true);
|
||||
|
||||
yield put($file, $keyPair->getPrivate());
|
||||
yield chmod($file, 0600);
|
||||
yield chown($file, 0, 0);
|
||||
} catch (FilesystemException $e) {
|
||||
throw new KeyStoreException("Could not save key.", 0, $e);
|
||||
}
|
||||
|
||||
return $keyPair;
|
||||
}
|
||||
}
|
||||
9
src/Stores/KeyStoreException.php
Normal file
9
src/Stores/KeyStoreException.php
Normal file
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Kelunik\AcmeClient\Stores;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class KeyStoreException extends RuntimeException {
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user