Files
acmeclient/src/functions.php
Niklas Keller 2dba4a852a Fix code style
2017-12-29 19:23:48 +01:00

281 lines
7.7 KiB
PHP

<?php
namespace Kelunik\AcmeClient;
use Amp\Sync\LocalSemaphore;
use Amp\Sync\Lock;
use InvalidArgumentException;
use Kelunik\Acme\AcmeException;
use Phar;
use Symfony\Component\Yaml\Exception\ParseException;
use Symfony\Component\Yaml\Yaml;
use function Amp\call;
use function Amp\coroutine;
function concurrentMap(int $concurrency, array $values, callable $functor): array {
$semaphore = new LocalSemaphore($concurrency);
return \array_map(coroutine(function ($value, $key) use ($semaphore, $functor) {
/** @var Lock $lock */
$lock = yield $semaphore->acquire();
try {
return yield call($functor, $value, $key);
} finally {
$lock->release();
}
}), $values, \array_keys($values));
}
/**
* Suggests a command based on similarity in a list of available commands.
*
* @param string $badCommand invalid command
* @param array $commands list of available commands
* @param int $suggestThreshold similarity threshold
*
* @return string suggestion or empty string if no command is similar enough
*/
function suggestCommand(string $badCommand, array $commands, int $suggestThreshold = 70): string {
$badCommand = \strtolower($badCommand);
$bestMatch = '';
$bestMatchPercentage = 0;
$byRefPercentage = 0;
foreach ($commands as $command) {
\similar_text($badCommand, \strtolower($command), $byRefPercentage);
if ($byRefPercentage > $bestMatchPercentage) {
$bestMatchPercentage = $byRefPercentage;
$bestMatch = $command;
}
}
return $bestMatchPercentage >= $suggestThreshold ? $bestMatch : '';
}
/**
* Resolves a server to a valid URI. If a valid shortcut is passed, it's resolved to the defined URI. If a URI without
* protocol is passed, it will default to HTTPS.
*
* @param string $uri URI to resolve
*
* @return string resolved URI
*/
function resolveServer(string $uri): string {
$shortcuts = [
'letsencrypt' => 'https://acme-v01.api.letsencrypt.org/directory',
'letsencrypt:production' => 'https://acme-v01.api.letsencrypt.org/directory',
'letsencrypt:staging' => 'https://acme-staging.api.letsencrypt.org/directory',
];
if (isset($shortcuts[$uri])) {
return $shortcuts[$uri];
}
if (\strpos($uri, '/') === false) {
throw new InvalidArgumentException('Invalid server URI: ' . $uri);
}
$protocol = \substr($uri, 0, \strpos($uri, '://'));
if (!$protocol || $protocol === $uri) {
return "https://{$uri}";
}
return $uri;
}
/**
* Transforms a directory URI to a valid filename for usage as key file name.
*
* @param string $server URI to the directory
*
* @return string identifier usable as file name
*/
function serverToKeyname(string $server): string {
$server = \substr($server, \strpos($server, '://') + 3);
$keyFile = \str_replace('/', '.', $server);
$keyFile = \preg_replace('@[^a-z0-9._-]@', '', $keyFile);
$keyFile = \preg_replace("@\\.+@", '.', $keyFile);
return $keyFile;
}
/**
* Checks whether the application is currently running as Phar.
*
* @return bool {@code true} if running as Phar, {@code false} otherwise
*/
function isPhar(): bool {
if (!\class_exists('Phar')) {
return false;
}
return Phar::running() !== '';
}
/**
* Normalizes a path. Replaces all backslashes with slashes and removes trailing slashes.
*
* @param string $path path to normalize
*
* @return string normalized path
*/
function normalizePath(string $path): string {
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()), \strlen('phar://')) . '/acme-client.yml'] : [];
if (0 !== \stripos(PHP_OS, '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): array {
$config = [];
if ($configPath = getConfigPath()) {
$configContent = \file_get_contents($configPath);
try {
$config = Yaml::parse($configContent);
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());
}
}
switch ($argument) {
case 'server':
$desc = [
'prefix' => 's',
'longPrefix' => 'server',
'description' => 'ACME server to use for registration and issuance of certificates.',
'required' => true,
];
if (isset($config['server'])) {
$desc['required'] = false;
$desc['defaultValue'] = $config['server'];
}
return $desc;
case 'storage':
$isPhar = isPhar();
$desc = [
'longPrefix' => 'storage',
'description' => 'Storage directory for account keys and certificates.',
'required' => $isPhar,
];
if (!$isPhar) {
$desc['defaultValue'] = \dirname(__DIR__) . '/data';
} elseif (isset($config['storage'])) {
$desc['required'] = false;
$desc['defaultValue'] = $config['storage'];
}
return $desc;
default:
throw new InvalidArgumentException('Unknown argument: ' . $argument);
}
}
/**
* Returns the binary that currently runs. Can be included in help texts about other commands.
*
* @return string binary callable, shortened based on PATH and CWD
*/
function getBinary(): string {
$binary = 'bin/acme';
if (isPhar()) {
$binary = \substr(Phar::running(), \strlen('phar://'));
$path = \getenv('PATH');
$locations = \explode(PATH_SEPARATOR, $path);
$binaryPath = \dirname($binary);
foreach ($locations as $location) {
if ($location === $binaryPath) {
return \substr($binary, \strlen($binaryPath) + 1);
}
}
$cwd = \getcwd();
if ($cwd && \strpos($binary, $cwd) === 0) {
$binary = '.' . \substr($binary, \strlen($cwd));
}
}
return $binary;
}
/**
* Cuts a text to a certain length and appends an ellipsis if necessary.
*
* @param string $text text to shorten
* @param int $max maximum length
* @param string $append appendix when too long
*
* @return string shortened string
*/
function ellipsis($text, $max = 70, $append = '…'): string {
if (\strlen($text) <= $max) {
return $text;
}
$out = \substr($text, 0, $max);
if (\strpos($text, ' ') === false) {
return $out . $append;
}
return \preg_replace("/\\w+$/", '', $out) . $append;
}