first commit

This commit is contained in:
2024-11-04 20:48:19 +01:00
commit 2fa33a3be9
7968 changed files with 2313063 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInita8412ede23fd11b4d0e29303fdebd5f4::getLoader();

View File

@@ -0,0 +1,479 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
private $vendorDir;
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
private $missingClasses = array();
private $apcuPrefix;
private static $registeredLoaders = array();
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

View File

@@ -0,0 +1,303 @@
<?php
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
class InstalledVersions
{
private static $installed = array (
'root' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '53cfe01c831d81b1398d479a9e85cbb4110e9e13',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '53cfe01c831d81b1398d479a9e85cbb4110e9e13',
),
'fbett/le_acme2' =>
array (
'pretty_version' => '1.5.6',
'version' => '1.5.6.0',
'aliases' =>
array (
),
'reference' => '26b2c421764b173326f6bcb0713a86bd614f77fa',
),
'plesk/api-php-lib' =>
array (
'pretty_version' => 'v1.0.7',
'version' => '1.0.7.0',
'aliases' =>
array (
),
'reference' => '7f81b0c3bb0a9f4200aef62a54d3e2c04d91a605',
),
),
);
private static $canGetVendors;
private static $installedByVendor = array();
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
public static function isInstalled($packageName)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return true;
}
}
return false;
}
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints($constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
public static function getRawData()
{
return self::$installed;
}
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
}
}
}
$installed[] = self::$installed;
return $installed;
}
}

View File

@@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,10 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'LE_ACME2' => array($vendorDir . '/fbett/le_acme2/src'),
);

View File

@@ -0,0 +1,10 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'PleskX\\' => array($vendorDir . '/plesk/api-php-lib/src'),
);

View File

@@ -0,0 +1,57 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInita8412ede23fd11b4d0e29303fdebd5f4
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInita8412ede23fd11b4d0e29303fdebd5f4', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInita8412ede23fd11b4d0e29303fdebd5f4', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
if ($useStaticLoader) {
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInita8412ede23fd11b4d0e29303fdebd5f4::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}

View File

@@ -0,0 +1,47 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInita8412ede23fd11b4d0e29303fdebd5f4
{
public static $prefixLengthsPsr4 = array (
'P' =>
array (
'PleskX\\' => 7,
),
);
public static $prefixDirsPsr4 = array (
'PleskX\\' =>
array (
0 => __DIR__ . '/..' . '/plesk/api-php-lib/src',
),
);
public static $prefixesPsr0 = array (
'L' =>
array (
'LE_ACME2' =>
array (
0 => __DIR__ . '/..' . '/fbett/le_acme2/src',
),
),
);
public static $classMap = array (
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInita8412ede23fd11b4d0e29303fdebd5f4::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInita8412ede23fd11b4d0e29303fdebd5f4::$prefixDirsPsr4;
$loader->prefixesPsr0 = ComposerStaticInita8412ede23fd11b4d0e29303fdebd5f4::$prefixesPsr0;
$loader->classMap = ComposerStaticInita8412ede23fd11b4d0e29303fdebd5f4::$classMap;
}, null, ClassLoader::class);
}
}

View File

@@ -0,0 +1 @@
<?php // You don't belong here. ?>

View File

@@ -0,0 +1,113 @@
{
"packages": [
{
"name": "fbett/le_acme2",
"version": "1.5.6",
"version_normalized": "1.5.6.0",
"source": {
"type": "git",
"url": "https://github.com/fbett/le-acme2-php.git",
"reference": "26b2c421764b173326f6bcb0713a86bd614f77fa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fbett/le-acme2-php/zipball/26b2c421764b173326f6bcb0713a86bd614f77fa",
"reference": "26b2c421764b173326f6bcb0713a86bd614f77fa",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-openssl": "*",
"php": ">=7.3"
},
"time": "2021-05-17T07:08:46+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"LE_ACME2": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabian Bett",
"homepage": "https://www.bett-ingenieure.de",
"role": "Developer"
}
],
"description": "Letsencrypt PHP ACME v2 client",
"homepage": "https://github.com/fbett/le-acme2-php",
"support": {
"issues": "https://github.com/fbett/le-acme2-php/issues",
"source": "https://github.com/fbett/le-acme2-php/tree/v1.5.6"
},
"install-path": "../fbett/le_acme2"
},
{
"name": "plesk/api-php-lib",
"version": "v1.0.7",
"version_normalized": "1.0.7.0",
"source": {
"type": "git",
"url": "https://github.com/plesk/api-php-lib.git",
"reference": "7f81b0c3bb0a9f4200aef62a54d3e2c04d91a605"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/plesk/api-php-lib/zipball/7f81b0c3bb0a9f4200aef62a54d3e2c04d91a605",
"reference": "7f81b0c3bb0a9f4200aef62a54d3e2c04d91a605",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-simplexml": "*",
"ext-xml": "*",
"php": "^7.3"
},
"require-dev": {
"phpunit/phpunit": "^9",
"spatie/phpunit-watcher": "^1.22"
},
"time": "2020-12-24T07:20:26+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"PleskX\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Alexei Yuzhakov",
"email": "sibprogrammer@gmail.com"
},
{
"name": "Plesk International GmbH.",
"email": "plesk-dev-leads@plesk.com"
}
],
"description": "PHP object-oriented library for Plesk XML-RPC API",
"support": {
"issues": "https://github.com/plesk/api-php-lib/issues",
"source": "https://github.com/plesk/api-php-lib/tree/v1.0.7"
},
"install-path": "../plesk/api-php-lib"
}
],
"dev": true,
"dev-package-names": []
}

View File

@@ -0,0 +1,42 @@
<?php return array (
'root' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '53cfe01c831d81b1398d479a9e85cbb4110e9e13',
'name' => '__root__',
),
'versions' =>
array (
'__root__' =>
array (
'pretty_version' => 'dev-master',
'version' => 'dev-master',
'aliases' =>
array (
),
'reference' => '53cfe01c831d81b1398d479a9e85cbb4110e9e13',
),
'fbett/le_acme2' =>
array (
'pretty_version' => '1.5.6',
'version' => '1.5.6.0',
'aliases' =>
array (
),
'reference' => '26b2c421764b173326f6bcb0713a86bd614f77fa',
),
'plesk/api-php-lib' =>
array (
'pretty_version' => 'v1.0.7',
'version' => '1.0.7.0',
'aliases' =>
array (
),
'reference' => '7f81b0c3bb0a9f4200aef62a54d3e2c04d91a605',
),
),
);

View File

@@ -0,0 +1,26 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 70100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
trigger_error(
'Composer detected issues in your platform: ' . implode(' ', $issues),
E_USER_ERROR
);
}

View File

@@ -0,0 +1 @@
<?php // You don't belong here. ?>

View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,119 @@
# le-acme2-php
LetsEncrypt client library for ACME v2 written in PHP.
This library is inspired by [yourivw/LEClient](https://github.com/yourivw/LEClient), completely rewritten and enhanced with some new features:
- Support for Composer autoload (including separated Namespaces)
- Automatic renewal process
- Managed HTTP authentication process
- Response caching mechanism
- Prevents blocking while waiting for server results
- Optional certificate feature "OCSP Must-Staple"
- Optional set a preferred chain
The aim of this client is to make an easy-to-use and integrated solution to create a LetsEncrypt-issued SSL/TLS certificate with PHP.
You have the possibility to use the HTTP authentication:
You need to be able to redirect specific requests (see below)
You have also the possibility to use DNS authentication:
You need to be able to set dynamic DNS configurations.
Wildcard certificates can only be requested by using the dns authentication.
## Current version
Tested with LetsEncrypt staging and production servers.
[Transitioning to ISRG's Root](https://letsencrypt.org/2019/04/15/transitioning-to-isrg-root.html):
This library supports it to set a preferred chain in `Order::setPreferredChain($issuerCN))`.
If the preferred chain is not set or set to IdenTrusts chain,
this library will try to use the IdenTrusts chain as long as possible.
Please see: https://letsencrypt.org/docs/dst-root-ca-x3-expiration-september-2021/
## Prerequisites
The minimum required PHP version is 7.3.
This client also depends on cURL and OpenSSL.
## Getting Started
Install via composer:
```
composer require fbett/le_acme2
```
Also have a look at the [LetsEncrypt documentation](https://letsencrypt.org/docs/) for more information and documentation on LetsEncrypt and ACME.
## Example Integration
- Create a working directory.
Warning: This directory will also include private keys, so i suggest to place this directory somewhere not in the root document path of the web server.
Additionally this directory should be protected to be read from other web server users.
```
mkdir /etc/ssl/le-storage/
chown root:root /etc/ssl/le-storage
chmod 0600 /etc/ssl/le-storage
```
- (HTTP authorization only) Create a directory for the acme challenges. It must be reachable by http/https.
```
mkdir /var/www/acme-challenges
```
- (HTTP authorization only) Redirect specific requests to your acme-challenges directory
Example apache virtual host configuration:
```
<VirtualHost ...>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule \.well-known/acme-challenge/(.*)$ https://your-domain.com/path/to/acme-challenges/$1 [R=302,L]
</IfModule>
</VirtualHost>
```
- (DNS authorization only) Set the DNS configuration
If `DNSWriter::write(...)` is called, set the DNS configuration like described in:
[https://letsencrypt.org/docs/challenge-types/#dns-01-challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
(By adding the digest as a TXT record for the subdomain '_acme-challenge'.)
- Use the certificate bundle, if the certificate is issued:
```
if($order->isCertificateBundleAvailable()) {
$bundle = $order->getCertificateBundle();
$pathToPrivateKey = $bundle->path . $bundle->private;
$pathToCertificate = $bundle->path . $bundle->certificate;
$pathToIntermediate = $bundle->path . $bundle->intermediate;
$order->enableAutoRenewal(); // If the date of expiration is closer than thirty days, the order will automatically start the renewal process.
}
```
If a certificate is renewed, the path will also change.
My integrated workflow is the following:
- User enables SSL to a specific domain in my control panel
- The cronjob of this control panel will detect these changes and tries to create or get an order like in the sample.
- The cronjob will fetch the information within the certificate bundle, if the certificate bundle is ready (mostly on the second run for challenge type HTTP and on the third run for challenge type DNS)
- The cronjob will also build the Apache virtual host files and will restart the Apache2 service, if the new config file is different.
Please take a look on the Samples for a full sample workflow.
## License
This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.

View File

@@ -0,0 +1,25 @@
{
"name": "fbett/le_acme2",
"description": "Letsencrypt PHP ACME v2 client",
"homepage": "https://github.com/fbett/le-acme2-php",
"version": "1.5.6",
"license": "MIT",
"authors": [
{
"name": "Fabian Bett",
"homepage": "https://www.bett-ingenieure.de",
"role": "Developer"
}
],
"autoload": {
"psr-0": {
"LE_ACME2": "src/"
}
},
"require": {
"php": ">=7.3",
"ext-curl": "*",
"ext-openssl": "*",
"ext-json": "*"
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
forceCoversAnnotation="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="true"
bootstrap="../../autoload.php"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
>
<coverage processUncoveredFiles="true">
<include>
<directory suffix=".php">./src/LE_ACME2</directory>
</include>
</coverage>
<testsuites>
<testsuite name="LE_ACME2 Test Suite">
<directory>./src/LE_ACME2Tests</directory>
</testsuite>
</testsuites>
</phpunit>

View File

@@ -0,0 +1,96 @@
<?php
namespace LE_ACME2;
defined('ABSPATH') or die();
use LE_ACME2\Connector\Connector;
abstract class AbstractKeyValuable {
const KEY_TYPE_RSA = "RSA";
const KEY_TYPE_EC = "EC";
protected $_identifier;
protected static $_directoryPath = null;
public static function setCommonKeyDirectoryPath(string $directoryPath) {
if(!file_exists($directoryPath)) {
throw new \RuntimeException('Common Key Directory Path does not exist');
}
self::$_directoryPath = realpath($directoryPath) . DIRECTORY_SEPARATOR;
}
public static function getCommonKeyDirectoryPath() : ?string {
return self::$_directoryPath;
}
protected function _getKeyDirectoryPath(string $appendix = '') : string {
return self::$_directoryPath . $this->_identifier . $appendix . DIRECTORY_SEPARATOR;
}
public function getKeyDirectoryPath() : string {
return $this->_getKeyDirectoryPath('');
}
protected function _initKeyDirectory(string $keyType = self::KEY_TYPE_RSA, bool $ignoreIfKeysExist = false) {
if(!file_exists($this->getKeyDirectoryPath())) {
mkdir($this->getKeyDirectoryPath());
}
if(!$ignoreIfKeysExist && (
file_exists($this->getKeyDirectoryPath() . 'private.pem') ||
file_exists($this->getKeyDirectoryPath() . 'public.pem')
)
) {
throw new \RuntimeException(
'Keys exist already. Exists the ' . get_class($this) . ' already?' . PHP_EOL .
'Path: ' . $this->getKeyDirectoryPath()
);
}
if($keyType == self::KEY_TYPE_RSA) {
Utilities\KeyGenerator::RSA(
$this->getKeyDirectoryPath(),
'private.pem',
'public.pem'
);
} else if($keyType == self::KEY_TYPE_EC) {
Utilities\KeyGenerator::EC(
$this->getKeyDirectoryPath(),
'private.pem',
'public.pem'
);
} else {
throw new \RuntimeException('Key type "' . $keyType . '" not supported.');
}
}
protected function _clearKeyDirectory() {
if(file_exists($this->getKeyDirectoryPath() . 'private.pem')) {
unlink($this->getKeyDirectoryPath() . 'private.pem');
}
if(file_exists($this->getKeyDirectoryPath() . 'public.pem')) {
unlink($this->getKeyDirectoryPath() . 'public.pem');
}
}
protected function _getAccountIdentifier(Account $account) : string {
$staging = Connector::getInstance()->isUsingStagingServer();
return 'account_' . ($staging ? 'staging_' : 'live_') . $account->getEmail();
}
}

View File

@@ -0,0 +1,163 @@
<?php
namespace LE_ACME2;
defined('ABSPATH') or die();
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
class Account extends AbstractKeyValuable {
private $_email = NULL;
public function __construct(string $email) {
$this->_setEmail($email);
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
get_class() . '::' . __FUNCTION__ . ' email: "' . $email . '"'
);
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' path: ' . $this->getKeyDirectoryPath()
);
}
private function _setEmail(string $email) {
$this->_email = $email;
$this->_identifier = $this->_getAccountIdentifier($this);
}
public function getEmail() : string {
return $this->_email;
}
/**
* @param string $email
* @return Account|null
* @throws Exception\AbstractException
*/
public static function create(string $email) : Account {
$account = new self($email);
$account->_initKeyDirectory();
$request = new Request\Account\Create($account);
try {
$response = $request->getResponse();
Cache\AccountResponse::getInstance()->set($account, $response);
return $account;
} catch(Exception\AbstractException $e) {
$account->_clearKeyDirectory();
throw $e;
}
}
public static function exists(string $email) : bool {
$account = new self($email);
return file_exists($account->getKeyDirectoryPath()) &&
file_exists($account->getKeyDirectoryPath() . 'private.pem') &&
file_exists($account->getKeyDirectoryPath() . 'public.pem');
}
public static function get(string $email) : Account {
$account = new self($email);
if(!self::exists($email))
throw new \RuntimeException('Keys not found - does this account exist?');
return $account;
}
/**
* @return Response\AbstractResponse|Response\Account\GetData
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getData() : Response\Account\GetData {
$request = new Request\Account\GetData($this);
return $request->getResponse();
}
/**
* @param string $email
* @return bool
* @throws Exception\RateLimitReached
*/
public function update(string $email) : bool {
$request = new Request\Account\Update($this, $email);
try {
/* $response = */ $request->getResponse();
$previousKeyDirectoryPath = $this->getKeyDirectoryPath();
$this->_setEmail($email);
if($previousKeyDirectoryPath != $this->getKeyDirectoryPath())
rename($previousKeyDirectoryPath, $this->getKeyDirectoryPath());
return true;
} catch(Exception\InvalidResponse $e) {
return false;
}
}
/**
* @return bool
* @throws Exception\RateLimitReached
*/
public function changeKeys() : bool {
Utilities\KeyGenerator::RSA($this->getKeyDirectoryPath(), 'private-replacement.pem', 'public-replacement.pem');
$request = new Request\Account\ChangeKeys($this);
try {
/* $response = */ $request->getResponse();
unlink($this->getKeyDirectoryPath() . 'private.pem');
unlink($this->getKeyDirectoryPath() . 'public.pem');
rename($this->getKeyDirectoryPath() . 'private-replacement.pem', $this->getKeyDirectoryPath() . 'private.pem');
rename($this->getKeyDirectoryPath() . 'public-replacement.pem', $this->getKeyDirectoryPath() . 'public.pem');
return true;
} catch(Exception\InvalidResponse $e) {
return false;
}
}
/**
* @return bool
* @throws Exception\RateLimitReached
*/
public function deactivate() : bool {
$request = new Request\Account\Deactivate($this);
try {
/* $response = */ $request->getResponse();
return true;
} catch(Exception\InvalidResponse $e) {
return false;
}
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace LE_ACME2\Authorizer;
defined('ABSPATH') or die();
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Cache;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
use LE_ACME2\Account;
use LE_ACME2\Order;
abstract class AbstractAuthorizer {
protected $_account;
protected $_order;
/**
* AbstractAuthorizer constructor.
*
* @param Account $account
* @param Order $order
*
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
public function __construct(Account $account, Order $order) {
$this->_account = $account;
$this->_order = $order;
$this->_fetchAuthorizationResponses();
}
/** @var Response\Authorization\Get[] $_authorizationResponses */
protected $_authorizationResponses = [];
/**
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
protected function _fetchAuthorizationResponses() {
if(!file_exists($this->_order->getKeyDirectoryPath() . 'private.pem')) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' result suppressed (Order has finished already)'
);
return;
}
$orderResponse = Cache\OrderResponse::getInstance()->get($this->_order);
foreach($orderResponse->getAuthorizations() as $authorization) {
$request = new Request\Authorization\Get($this->_account, $authorization);
$this->_authorizationResponses[] = $request->getResponse();
}
}
protected function _hasValidAuthorizationResponses() : bool {
return count($this->_authorizationResponses) > 0;
}
public function shouldStartAuthorization() : bool {
foreach($this->_authorizationResponses as $response) {
$challenge = $response->getChallenge($this->_getChallengeType());
if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_PENDING) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' "Pending challenge found',
$challenge
);
return true;
}
}
return false;
}
abstract protected function _getChallengeType() : string;
/**
* @throws Exception\AuthorizationInvalid
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
public function progress() {
if(!$this->_hasValidAuthorizationResponses())
return;
$existsNotValidChallenges = false;
foreach($this->_authorizationResponses as $authorizationResponse) {
$challenge = $authorizationResponse->getChallenge($this->_getChallengeType());
if($this->_existsNotValidChallenges($challenge, $authorizationResponse)) {
$existsNotValidChallenges = true;
}
}
$this->_finished = !$existsNotValidChallenges;
}
/**
* @param Response\Authorization\Struct\Challenge $challenge
* @param Response\Authorization\Get $authorizationResponse
* @return bool
*
* @throws Exception\AuthorizationInvalid
*/
protected function _existsNotValidChallenges(Response\Authorization\Struct\Challenge $challenge,
Response\Authorization\Get $authorizationResponse
) : bool {
if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_PENDING) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' "Non valid challenge found',
$challenge
);
return true;
}
else if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_PROGRESSING) {
// Should come back later
return true;
}
else if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_VALID) {
}
else if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_INVALID) {
throw new Exception\AuthorizationInvalid(
'Received status "' . Response\Authorization\Struct\Challenge::STATUS_INVALID . '" while challenge should be verified'
);
}
else {
throw new \RuntimeException('Challenge status "' . $challenge->status . '" is not implemented');
}
return false;
}
protected $_finished = false;
public function hasFinished() : bool {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_called_class() . '::' . __FUNCTION__,
$this->_finished
);
return $this->_finished;
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace LE_ACME2\Authorizer;
defined('ABSPATH') or die();
use LE_ACME2\Order;
abstract class AbstractDNSWriter {
/**
* @param Order $order
* @param string $identifier
* @param string $digest
*
* @return bool return true, if the dns configuration is usable and the process should be progressed
*/
abstract public function write(Order $order, string $identifier, string $digest) : bool;
}

View File

@@ -0,0 +1,70 @@
<?php
namespace LE_ACME2\Authorizer;
defined('ABSPATH') or die();
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Exception;
use LE_ACME2\Order;
use LE_ACME2\Struct\ChallengeAuthorizationKey;
use LE_ACME2\Utilities;
class DNS extends AbstractAuthorizer {
protected function _getChallengeType(): string {
return Order::CHALLENGE_TYPE_DNS;
}
/** @var AbstractDNSWriter $_dnsWriter */
private static $_dnsWriter = null;
public static function setWriter(AbstractDNSWriter $dnsWriter) : void {
self::$_dnsWriter = $dnsWriter;
}
/**
* @param Response\Authorization\Struct\Challenge $challenge
* @param Response\Authorization\Get $authorizationResponse
* @return bool
*
* @throws Exception\AuthorizationInvalid
* @throws Exception\DNSAuthorizationInvalid
* @throws Exception\ExpiredAuthorization
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
protected function _existsNotValidChallenges(Response\Authorization\Struct\Challenge $challenge,
Response\Authorization\Get $authorizationResponse
) : bool {
if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_PENDING) {
if(self::$_dnsWriter === null) {
throw new \RuntimeException('DNS writer is not set');
}
if( self::$_dnsWriter->write(
$this->_order,
$authorizationResponse->getIdentifier()->value,
(new ChallengeAuthorizationKey($this->_account))->getEncoded($challenge->token)
)
) {
$request = new Request\Authorization\Start($this->_account, $this->_order, $challenge);
/* $response = */ $request->getResponse();
} else {
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_INFO, 'Pending challenge deferred');
}
}
if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_INVALID) {
throw new Exception\DNSAuthorizationInvalid(
'Received status "' . Response\Authorization\Struct\Challenge::STATUS_INVALID . '" while challenge should be verified'
);
}
return parent::_existsNotValidChallenges($challenge, $authorizationResponse);
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace LE_ACME2\Authorizer;
defined('ABSPATH') or die();
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Struct\ChallengeAuthorizationKey;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
use LE_ACME2\Order;
class HTTP extends AbstractAuthorizer {
protected static $_directoryPath = null;
public static function setDirectoryPath(string $directoryPath) {
if(!file_exists($directoryPath)) {
throw new \RuntimeException('HTTP authorization directory path does not exist');
}
self::$_directoryPath = realpath($directoryPath) . DIRECTORY_SEPARATOR;
}
public static function getDirectoryPath() : ?string {
return self::$_directoryPath;
}
protected function _getChallengeType(): string {
return Order::CHALLENGE_TYPE_HTTP;
}
/**
* @param Response\Authorization\Struct\Challenge $challenge
* @param Response\Authorization\Get $authorizationResponse
* @return bool
*
* @throws Exception\AuthorizationInvalid
* @throws Exception\ExpiredAuthorization
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
protected function _existsNotValidChallenges(Response\Authorization\Struct\Challenge $challenge,
Response\Authorization\Get $authorizationResponse
) : bool {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
'Challenge "' . $challenge->token . '" has status:' . $challenge->status
);
if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_PENDING) {
$this->_writeToFile($challenge);
if($this->_validateFile($authorizationResponse->getIdentifier()->value, $challenge)) {
$request = new Request\Authorization\Start($this->_account, $this->_order, $challenge);
/* $response = */ $request->getResponse();
} else {
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_INFO, 'Could not validate HTTP Authorization file');
}
}
if($challenge->status == Response\Authorization\Struct\Challenge::STATUS_INVALID) {
throw new Exception\HTTPAuthorizationInvalid(
'Received status "' . Response\Authorization\Struct\Challenge::STATUS_INVALID . '" while challenge should be verified'
);
}
return parent::_existsNotValidChallenges($challenge, $authorizationResponse);
}
private function _writeToFile(Response\Authorization\Struct\Challenge $challenge) : void {
file_put_contents(
self::$_directoryPath . $challenge->token,
(new ChallengeAuthorizationKey($this->_account))->get($challenge->token)
);
}
/**
* @param string $domain
* @param Response\Authorization\Struct\Challenge $challenge
* @return bool
*
* @throws Exception\HTTPAuthorizationInvalid
*/
private function _validateFile(string $domain, Response\Authorization\Struct\Challenge $challenge) : bool {
if ( get_option('rsssl_skip_challenge_directory_request') ) {
return true;
}
$challengeAuthorizationKey = new ChallengeAuthorizationKey($this->_account);
$requestURL = 'http://' . $domain . '/.well-known/acme-challenge/' . $challenge->token;
$handle = curl_init();
curl_setopt($handle, CURLOPT_URL, $requestURL);
curl_setopt($handle, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($handle);
$result = !empty($response) && $response == $challengeAuthorizationKey->get($challenge->token);
if(!$result) {
throw new Exception\HTTPAuthorizationInvalid(
'HTTP challenge for "' . $domain . '"": ' .
$domain . '/.well-known/acme-challenge/' . $challenge->token .
' tested, found invalid. CURL response: ' . var_export($response, true)
);
}
return true;
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace LE_ACME2\Cache;
defined('ABSPATH') or die();
use LE_ACME2\AbstractKeyValuable;
abstract class AbstractKeyValuableCache {
protected function __construct() {}
protected function _getObjectIdentifier(AbstractKeyValuable $object) : string {
return $object->getKeyDirectoryPath();
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace LE_ACME2\Cache;
defined('ABSPATH') or die();
use LE_ACME2\Account;
use LE_ACME2\Connector;
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
use LE_ACME2\SingletonTrait;
class AccountResponse extends AbstractKeyValuableCache {
use SingletonTrait;
private const _FILE = 'CacheResponse';
private const _DEPRECATED_FILE = 'DirectoryNewAccountResponse';
private $_responses = [];
/**
* @param Account $account
* @return Response\Account\AbstractAccount
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function get(Account $account): Response\Account\AbstractAccount {
$accountIdentifier = $this->_getObjectIdentifier($account);
if(array_key_exists($accountIdentifier, $this->_responses)) {
return $this->_responses[ $accountIdentifier ];
}
$this->_responses[ $accountIdentifier ] = null;
$cacheFile = $account->getKeyDirectoryPath() . self::_FILE;
$deprecatedCacheFile = $account->getKeyDirectoryPath() . self::_DEPRECATED_FILE;
if(file_exists($deprecatedCacheFile) && !file_exists($cacheFile)) {
rename($deprecatedCacheFile, $cacheFile);
}
if(file_exists($cacheFile) && filemtime($cacheFile) > strtotime('-7 days')) {
$rawResponse = Connector\RawResponse::getFromString(file_get_contents($cacheFile));
$response = new Response\Account\Create($rawResponse);
$this->_responses[ $accountIdentifier ] = $response;
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' response from cache'
);
return $response;
}
$request = new Request\Account\Get($account);
$response = $request->getResponse();
$this->set($account, $response);
return $response;
}
public function set(Account $account, Response\Account\AbstractAccount $response = null) : void {
$accountIdentifier = $this->_getObjectIdentifier($account);
$filePath = $account->getKeyDirectoryPath() . self::_FILE;
if($response === null) {
unset($this->_responses[$accountIdentifier]);
if(file_exists($filePath)) {
unlink($filePath);
}
return;
}
$this->_responses[$accountIdentifier] = $response;
file_put_contents($filePath, $response->getRaw()->toString());
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace LE_ACME2\Cache;
defined('ABSPATH') or die();
use LE_ACME2\Connector;
use LE_ACME2\Account;
use LE_ACME2\SingletonTrait;
use LE_ACME2\Exception;
use LE_ACME2\Request;
use LE_ACME2\Response;
class DirectoryResponse {
use SingletonTrait;
private const _FILE = 'DirectoryResponse';
private function __construct() {}
private $_responses = [];
private $_index = 0;
/**
* @return Response\GetDirectory
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function get() : Response\GetDirectory {
if(array_key_exists($this->_index, $this->_responses)) {
return $this->_responses[$this->_index];
}
$this->_responses[$this->_index] = null;
$cacheFile = Account::getCommonKeyDirectoryPath() . self::_FILE;
if(file_exists($cacheFile) && filemtime($cacheFile) > strtotime('-2 days')) {
$rawResponse = Connector\RawResponse::getFromString(file_get_contents($cacheFile));
try {
return $this->_responses[$this->_index] = new Response\GetDirectory($rawResponse);
} catch(Exception\AbstractException $e) {
unlink($cacheFile);
}
}
$request = new Request\GetDirectory();
$response = $request->getResponse();
$this->set($response);
return $response;
}
public function set(Response\GetDirectory $response) : void {
$cacheFile = Account::getCommonKeyDirectoryPath() . self::_FILE;
$this->_responses[$this->_index] = $response;
file_put_contents($cacheFile, $response->getRaw()->toString());
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace LE_ACME2\Cache;
defined('ABSPATH') or die();
use LE_ACME2\SingletonTrait;
use LE_ACME2\Exception;
use LE_ACME2\Request;
use LE_ACME2\Response;
class NewNonceResponse {
use SingletonTrait;
private function __construct() {}
private $_responses = [];
private $_index = 0;
/**
* @return Response\GetNewNonce
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function get() : Response\GetNewNonce {
if(array_key_exists($this->_index, $this->_responses)) {
return $this->_responses[$this->_index];
}
$this->_responses[$this->_index] = null;
$request = new Request\GetNewNonce();
$response = $request->getResponse();
$this->set($response);
return $response;
}
public function set(Response\GetNewNonce $response) : void {
$this->_responses[$this->_index] = $response;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace LE_ACME2\Cache;
defined('ABSPATH') or die();
use LE_ACME2\Connector;
use LE_ACME2\Order;
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
use LE_ACME2\SingletonTrait;
class OrderResponse extends AbstractKeyValuableCache {
use SingletonTrait;
private const _FILE = 'CacheResponse';
private const _DEPRECATED_FILE = 'DirectoryNewOrderResponse';
private $_responses = [];
public function exists(Order $order) : bool {
$cacheFile = $order->getKeyDirectoryPath() . self::_FILE;
$deprecatedCacheFile = $order->getKeyDirectoryPath() . self::_DEPRECATED_FILE;
return file_exists($cacheFile) || file_exists($deprecatedCacheFile);
}
/**
* @param Order $order
* @return Response\Order\AbstractOrder
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function get(Order $order): Response\Order\AbstractOrder {
$accountIdentifier = $this->_getObjectIdentifier($order->getAccount());
$orderIdentifier = $this->_getObjectIdentifier($order);
if(!isset($this->_responses[$accountIdentifier])) {
$this->_responses[$accountIdentifier] = [];
}
if(array_key_exists($orderIdentifier, $this->_responses[$accountIdentifier])) {
return $this->_responses[ $accountIdentifier ][ $orderIdentifier ];
}
$this->_responses[ $accountIdentifier ][ $orderIdentifier ] = null;
$cacheFile = $order->getKeyDirectoryPath() . self::_FILE;
$deprecatedCacheFile = $order->getKeyDirectoryPath() . self::_DEPRECATED_FILE;
if(file_exists($deprecatedCacheFile) && !file_exists($cacheFile)) {
rename($deprecatedCacheFile, $cacheFile);
}
if(file_exists($cacheFile)) {
$rawResponse = Connector\RawResponse::getFromString(file_get_contents($cacheFile));
$response = new Response\Order\Create($rawResponse);
if(
$response->getStatus() != Response\Order\AbstractOrder::STATUS_VALID
) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' (cache did not satisfy, status "' . $response->getStatus() . '")'
);
$request = new Request\Order\Get($order, $response);
$response = $request->getResponse();
$this->set($order, $response);
return $response;
}
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' (from cache, status "' . $response->getStatus() . '")'
);
$this->_responses[$accountIdentifier][$orderIdentifier] = $response;
return $response;
}
throw new \RuntimeException(
self::_FILE . ' could not be found for order: ' .
'- Path: ' . $order->getKeyDirectoryPath() . PHP_EOL .
'- Subjects: ' . var_export($order->getSubjects(), true) . PHP_EOL
);
}
public function set(Order $order, Response\Order\AbstractOrder $response = null) : void {
$accountIdentifier = $this->_getObjectIdentifier($order->getAccount());
$orderIdentifier = $this->_getObjectIdentifier($order);
$filePath = $order->getKeyDirectoryPath() . self::_FILE;
if($response === null) {
unset($this->_responses[$accountIdentifier][$orderIdentifier]);
if(file_exists($filePath)) {
unlink($filePath);
}
return;
}
$this->_responses[$accountIdentifier][$orderIdentifier] = $response;
file_put_contents($filePath, $response->getRaw()->toString());
}
}

View File

@@ -0,0 +1,111 @@
<?php
namespace LE_ACME2\Connector;
defined('ABSPATH') or die();
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\SingletonTrait;
use LE_ACME2\Cache;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
class Connector {
use SingletonTrait;
const METHOD_GET = 'GET';
const METHOD_HEAD = 'HEAD';
const METHOD_POST = 'POST';
private function __construct() {}
protected $_baseURL = 'https://acme-v02.api.letsencrypt.org';
protected $_stagingBaseURL = 'https://acme-staging-v02.api.letsencrypt.org';
protected $_useStagingServer = true;
public function useStagingServer(bool $useStagingServer) {
$this->_useStagingServer = $useStagingServer;
}
public function isUsingStagingServer() : bool {
return $this->_useStagingServer;
}
public function getBaseURL() : string {
return $this->_useStagingServer ? $this->_stagingBaseURL : $this->_baseURL;
}
/**
* Makes a Curl request.
*
* @param string $method The HTTP method to use. Accepting GET, POST and HEAD requests.
* @param string $url The URL to make the request to.
* @param string $data The body to attach to a POST request. Expected as a JSON encoded string.
*
* @return RawResponse
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function request(string $method, string $url, string $data = null) : RawResponse {
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_INFO, 'will request from ' . $url, $data);
$handle = curl_init();
$headers = array(
'Accept: application/json',
'Content-Type: ' . ($method == self::METHOD_POST ? 'application/jose+json' : 'application/json') // ACME draft-10, section 6.2
);
curl_setopt($handle, CURLOPT_URL, $url);
curl_setopt($handle, CURLOPT_HTTPHEADER, $headers);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handle, CURLOPT_HEADER, true);
switch ($method) {
case self::METHOD_GET:
break;
case self::METHOD_POST:
curl_setopt($handle, CURLOPT_POST, true);
curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
break;
case self::METHOD_HEAD:
curl_setopt($handle, CURLOPT_CUSTOMREQUEST, 'HEAD');
curl_setopt($handle, CURLOPT_NOBODY, true);
break;
default:
throw new \RuntimeException('HTTP request ' . $method . ' not supported.');
break;
}
$response = curl_exec($handle);
if(curl_errno($handle)) {
throw new \RuntimeException('Curl: ' . curl_error($handle));
}
$header_size = curl_getinfo($handle, CURLINFO_HEADER_SIZE);
$rawResponse = new RawResponse();
$rawResponse->init($method, $url, $response, $header_size);
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_INFO, self::class . ': response received', $rawResponse);
try {
$getNewNonceResponse = new Response\GetNewNonce($rawResponse);
Cache\NewNonceResponse::getInstance()->set($getNewNonceResponse);
} catch(Exception\InvalidResponse $e) {
if($method == self::METHOD_POST) {
$request = new Request\GetNewNonce();
Cache\NewNonceResponse::getInstance()->set($request->getResponse());
}
}
return $rawResponse;
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace LE_ACME2\Connector;
defined('ABSPATH') or die();
class RawResponse {
/** @var string */
public $request;
/** @var array */
public $header;
/** @var array|string */
public $body;
public function init(string $method, string $url, string $response, int $headerSize) {
$header = substr($response, 0, $headerSize);
$body = substr($response, $headerSize);
$body_json = json_decode($body, true);
$this->request = $method . ' ' . $url;
$this->header = array_map(function($line) {
return trim($line);
}, explode("\n", $header));
$this->body = $body_json === null ? $body : $body_json;
}
public function toString() : string {
return serialize([
'request' => $this->request,
'header' => $this->header,
'body' => $this->body,
]);
}
public static function getFromString(string $string) : self {
$array = unserialize($string);
$rawResponse = new self();
$rawResponse->request = $array['request'];
$rawResponse->header = $array['header'];
$rawResponse->body = $array['body'];
return $rawResponse;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
use LE_ACME2\Utilities;
abstract class AbstractException extends \Exception {
public function __construct(string $message) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
'Exception "' . get_called_class() . '" thrown '
);
parent::__construct($message);
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class AuthorizationInvalid extends AbstractException {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class DNSAuthorizationInvalid extends AuthorizationInvalid {}

View File

@@ -0,0 +1,11 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class ExpiredAuthorization extends AbstractException {
public function __construct() {
parent::__construct("Expired authorization received");
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class HTTPAuthorizationInvalid extends AuthorizationInvalid {}

View File

@@ -0,0 +1,40 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
use LE_ACME2\Connector\RawResponse;
class InvalidResponse extends AbstractException {
private $_rawResponse;
private $_responseStatus;
public function __construct(RawResponse $rawResponse, string $responseStatus = null) {
$this->_rawResponse = $rawResponse;
$this->_responseStatus = $responseStatus;
if($responseStatus === '') {
$responseStatus = 'Unknown response status';
}
if(isset($this->_rawResponse->body['type'])) {
$responseStatus = $this->_rawResponse->body['type'];
}
if(isset($this->_rawResponse->body['detail'])) {
$responseStatus .= ' - ' . $this->_rawResponse->body['detail'];
}
parent::__construct('Invalid response received: ' . $responseStatus);
}
public function getRawResponse() : RawResponse {
return $this->_rawResponse;
}
public function getResponseStatus() : ?string {
return $this->_responseStatus;
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class OpenSSLException extends AbstractException {
public function __construct(string $function) {
$errors = [];
while(($error = openssl_error_string()) !== false) {
$errors[] = $error;
}
parent::__construct(
$function . ' failed - error messages: ' . var_export($errors, true)
);
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class RateLimitReached extends AbstractException {
public function __construct(string $request, string $detail) {
parent::__construct(
"Invalid response received for request (" . $request . "): " .
"rate limit reached - " . $detail
);
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Exception;
defined('ABSPATH') or die();
class StatusInvalid extends AbstractException {}

View File

@@ -0,0 +1,438 @@
<?php
namespace LE_ACME2;
defined('ABSPATH') or die();
use LE_ACME2\Request;
use LE_ACME2\Response;
use LE_ACME2\Cache;
use LE_ACME2\Authorizer;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
class Order extends AbstractKeyValuable {
const CHALLENGE_TYPE_HTTP = 'http-01';
const CHALLENGE_TYPE_DNS = 'dns-01';
/**
* @deprecated
* @param $directoryPath
*/
public static function setHTTPAuthorizationDirectoryPath(string $directoryPath) {
Authorizer\HTTP::setDirectoryPath($directoryPath);
}
CONST IDENTRUST_ISSUER_CN = 'DST Root CA X3';
/** @var string|null $_preferredChain */
private static $_preferredChain = null;
public static function setPreferredChain(string $issuerCN = null) {
self::$_preferredChain = $issuerCN;
}
protected $_account;
protected $_subjects;
public function __construct(Account $account, array $subjects) {
array_map(function($subject) {
if(preg_match_all('~(\*\.)~', $subject) > 1)
throw new \RuntimeException('Cannot create orders with multiple wildcards in one domain.');
}, $subjects);
$this->_account = $account;
$this->_subjects = $subjects;
$this->_identifier = $this->_getAccountIdentifier($account) . DIRECTORY_SEPARATOR .
'order_' . md5(implode('|', $subjects));
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
get_class() . '::' . __FUNCTION__ . ' "' . implode(':', $this->getSubjects()) . '"'
);
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_DEBUG,
get_class() . '::' . __FUNCTION__ . ' path: ' . $this->getKeyDirectoryPath()
);
}
public function getAccount() : Account {
return $this->_account;
}
public function getSubjects() : array {
return $this->_subjects;
}
/**
* @param Account $account
* @param array $subjects
* @param string $keyType
* @return Order
* @throws Exception\AbstractException
*/
public static function create(Account $account, array $subjects, string $keyType = self::KEY_TYPE_RSA) : Order {
$order = new self($account, $subjects);
return $order->_create($keyType, false);
}
/**
* @param $keyType
* @param bool $ignoreIfKeysExist
* @return Order
* @throws Exception\AbstractException
*/
protected function _create(string $keyType, bool $ignoreIfKeysExist = false) : Order {
$this->_initKeyDirectory($keyType, $ignoreIfKeysExist);
$request = new Request\Order\Create($this);
try {
$response = $request->getResponse();
Cache\OrderResponse::getInstance()->set($this, $response);
return $this;
} catch(Exception\AbstractException $e) {
$this->_clearKeyDirectory();
throw $e;
}
}
public static function exists(Account $account, array $subjects) : bool {
$order = new self($account, $subjects);
return Cache\OrderResponse::getInstance()->exists($order);
}
public static function get(Account $account, array $subjects) : Order {
$order = new self($account, $subjects);
if(!self::exists($account, $subjects))
throw new \RuntimeException('Order does not exist');
return $order;
}
/** @var Authorizer\AbstractAuthorizer|Authorizer\HTTP|null $_authorizer */
protected $_authorizer = null;
/**
* @param $type
* @return Authorizer\AbstractAuthorizer|Authorizer\HTTP|null
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
protected function _getAuthorizer(string $type) : Authorizer\AbstractAuthorizer {
if($this->_authorizer === null) {
if($type == self::CHALLENGE_TYPE_HTTP) {
$this->_authorizer = new Authorizer\HTTP($this->_account, $this);
} else if($type == self::CHALLENGE_TYPE_DNS) {
$this->_authorizer = new Authorizer\DNS($this->_account, $this);
} else {
throw new \RuntimeException('Challenge type not implemented');
}
}
return $this->_authorizer;
}
/**
* The Authorization has expired, so we clean the complete order to restart again on the next call
*/
protected function _clearAfterExpiredAuthorization() {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
get_class() . '::' . __FUNCTION__ . ' "Will clear after expired authorization'
);
$this->clear();
}
public function clear() {
Cache\OrderResponse::getInstance()->set($this, null);
$this->_clearKeyDirectory();
}
/**
* @return bool
* @param $type
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function shouldStartAuthorization(string $type) : bool {
try {
return $this->_getAuthorizer($type)->shouldStartAuthorization();
} catch(Exception\ExpiredAuthorization $e) {
$this->_clearAfterExpiredAuthorization();
return false;
}
}
/**
* @param $type
* @return bool
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\AuthorizationInvalid
*/
public function authorize(string $type) : bool {
try {
$authorizer = $this->_getAuthorizer($type);
$authorizer->progress();
return $authorizer->hasFinished();
} catch(Exception\ExpiredAuthorization $e) {
$this->_clearAfterExpiredAuthorization();
return false;
}
}
/**
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function finalize() {
if(!is_object($this->_authorizer) || !$this->_authorizer->hasFinished()) {
throw new \RuntimeException('Not all challenges are valid. Please check result of authorize() first!');
}
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
get_class() . '::' . __FUNCTION__ . ' "Will finalize'
);
$orderResponse = Cache\OrderResponse::getInstance()->get($this);
if(
$orderResponse->getStatus() == Response\Order\AbstractOrder::STATUS_PENDING /* DEPRECATED AFTER JULI 5TH 2018 */ ||
$orderResponse->getStatus() == Response\Order\AbstractOrder::STATUS_READY // ACME draft-12 Section 7.1.6
) {
$request = new Request\Order\Finalize($this, $orderResponse);
$orderResponse = $request->getResponse();
Cache\OrderResponse::getInstance()->set($this, $orderResponse);
}
if($orderResponse->getStatus() == Response\Order\AbstractOrder::STATUS_VALID) {
$request = new Request\Order\GetCertificate($this, $orderResponse);
$response = $request->getResponse();
$certificate = $response->getCertificate();
$intermediate = $response->getIntermediate();
//$certificateInfo = openssl_x509_parse($certificate);
//$certificateValidToTimeTimestamp = $certificateInfo['validTo_time_t'];
$intermediateInfo = openssl_x509_parse($intermediate);
if(self::$_preferredChain !== null) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
'Preferred chain is set: ' . self::$_preferredChain
);
}
$found = false;
if(self::$_preferredChain !== null && $intermediateInfo['issuer']['CN'] != self::$_preferredChain) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
'Default certificate does not satisfy preferred chain, trying to fetch alternative'
);
foreach($response->getAlternativeLinks() as $link) {
$request = new Request\Order\GetCertificate($this, $orderResponse, $link);
$response = $request->getResponse();
$alternativeCertificate = $response->getCertificate();
$alternativeIntermediate = $response->getIntermediate();
$intermediateInfo = openssl_x509_parse($intermediate);
if($intermediateInfo['issuer']['CN'] != self::$_preferredChain) {
continue;
}
$found = true;
$certificate = $alternativeCertificate;
$intermediate = $alternativeIntermediate;
break;
}
if(!$found) {
Utilities\Logger::getInstance()->add(
Utilities\Logger::LEVEL_INFO,
'Preferred chain could not be satisfied, returning default chain'
);
}
}
$this->_saveCertificate($certificate, $intermediate);
}
}
private function _saveCertificate(string $certificate, string $intermediate) : void {
$certificateInfo = openssl_x509_parse($certificate);
$certificateValidToTimeTimestamp = $certificateInfo['validTo_time_t'];
$path = $this->getKeyDirectoryPath() . self::BUNDLE_DIRECTORY_PREFIX . $certificateValidToTimeTimestamp . DIRECTORY_SEPARATOR;
mkdir($path);
rename($this->getKeyDirectoryPath() . 'private.pem', $path . 'private.pem');
file_put_contents($path . 'certificate.crt', $certificate);
file_put_contents($path . 'intermediate.pem', $intermediate);
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_INFO, 'Certificate received');
}
const BUNDLE_DIRECTORY_PREFIX = 'bundle_';
protected function _getLatestCertificateDirectory() : ?string {
$files = scandir($this->getKeyDirectoryPath(), SORT_NUMERIC | SORT_DESC);
foreach($files as $file) {
if(
substr($file, 0, strlen(self::BUNDLE_DIRECTORY_PREFIX)) == self::BUNDLE_DIRECTORY_PREFIX &&
is_dir($this->getKeyDirectoryPath() . $file)
) {
return $file;
}
}
return null;
}
public function isCertificateBundleAvailable() : bool {
return $this->_getLatestCertificateDirectory() !== NULL;
}
public function getCertificateBundle() : Struct\CertificateBundle {
if(!$this->isCertificateBundleAvailable()) {
throw new \RuntimeException('There is no certificate available');
}
$certificatePath = $this->getKeyDirectoryPath() . $this->_getLatestCertificateDirectory();
return new Struct\CertificateBundle(
$certificatePath . DIRECTORY_SEPARATOR,
'private.pem',
'certificate.crt',
'intermediate.pem',
self::_getExpireTimeFromCertificateDirectoryPath($certificatePath)
);
}
/**
* @param string $keyType
* @param int|null $renewBefore Unix timestamp
* @throws Exception\AbstractException
*/
public function enableAutoRenewal($keyType = self::KEY_TYPE_RSA, int $renewBefore = null) {
if($keyType === null) {
$keyType = self::KEY_TYPE_RSA;
}
if(!$this->isCertificateBundleAvailable()) {
throw new \RuntimeException('There is no certificate available');
}
$orderResponse = Cache\OrderResponse::getInstance()->get($this);
if(
$orderResponse === null ||
$orderResponse->getStatus() != Response\Order\AbstractOrder::STATUS_VALID
) {
return;
}
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_DEBUG,'Auto renewal triggered');
$directory = $this->_getLatestCertificateDirectory();
$expireTime = self::_getExpireTimeFromCertificateDirectoryPath($directory);
if($renewBefore === null) {
$renewBefore = strtotime('-30 days', $expireTime);
}
if($renewBefore < time()) {
Utilities\Logger::getInstance()->add(Utilities\Logger::LEVEL_INFO,'Auto renewal: Will recreate order');
$this->_create($keyType, true);
}
}
/**
* @param int $reason The reason to revoke the LetsEncrypt Order instance certificate.
* Possible reasons can be found in section 5.3.1 of RFC5280.
* @return bool
* @throws Exception\RateLimitReached
*/
public function revokeCertificate(int $reason = 0) : bool {
if(!$this->isCertificateBundleAvailable()) {
throw new \RuntimeException('There is no certificate available to revoke');
}
$bundle = $this->getCertificateBundle();
$request = new Request\Order\RevokeCertificate($bundle, $reason);
try {
/* $response = */ $request->getResponse();
rename(
$this->getKeyDirectoryPath(),
$this->_getKeyDirectoryPath('-revoked-' . microtime(true))
);
return true;
} catch(Exception\InvalidResponse $e) {
return false;
}
}
protected static function _getExpireTimeFromCertificateDirectoryPath(string $path) {
$stringPosition = strrpos($path, self::BUNDLE_DIRECTORY_PREFIX);
if($stringPosition === false) {
throw new \RuntimeException('ExpireTime not found in' . $path);
}
$expireTime = substr($path, $stringPosition + strlen(self::BUNDLE_DIRECTORY_PREFIX));
if(
!is_numeric($expireTime) ||
$expireTime < strtotime('-10 years') ||
$expireTime > strtotime('+10 years')
) {
throw new \RuntimeException('Unexpected expireTime: ' . $expireTime);
}
return $expireTime;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace LE_ACME2\Request;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
use LE_ACME2\Exception;
abstract class AbstractRequest {
/**
* @return AbstractResponse
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
abstract public function getResponse() : AbstractResponse;
protected function _buildContactPayload(string $email) : array {
$result = [
'mailto:' . $email
];
return $result;
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
use LE_ACME2\Account;
abstract class AbstractLocation extends AbstractRequest {
protected $_account;
public function __construct(Account $account) {
$this->_account = $account;
}
/**
* @return Connector\RawResponse
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
protected function _getRawResponse() : Connector\RawResponse {
$payload = $this->_getPayload();
if(count($payload) == 0) {
$payload['rand-' . rand(100000, 1000000)] = 1;
}
$kid = Utilities\RequestSigner::KID(
$payload,
Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
$kid
);
return $result;
}
abstract protected function _getPayload() : array;
}

View File

@@ -0,0 +1,84 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
use LE_ACME2\Account;
class ChangeKeys extends AbstractRequest {
protected $_account;
public function __construct(Account $account) {
$this->_account = $account;
}
/**
* @return Response\AbstractResponse|Response\Account\ChangeKeys
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$currentPrivateKey = openssl_pkey_get_private(
file_get_contents($this->_account->getKeyDirectoryPath() . 'private.pem')
);
$currentPrivateKeyDetails = openssl_pkey_get_details($currentPrivateKey);
/**
* draft-13 Section 7.3.6
* "newKey" is deprecated after August 23rd 2018
*/
$newPrivateKey = openssl_pkey_get_private(
file_get_contents($this->_account->getKeyDirectoryPath() . 'private-replacement.pem')
);
$newPrivateKeyDetails = openssl_pkey_get_details($newPrivateKey);
$innerPayload = [
'account' => Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
'oldKey' => [
"kty" => "RSA",
"n" => Utilities\Base64::UrlSafeEncode($currentPrivateKeyDetails["rsa"]["n"]),
"e" => Utilities\Base64::UrlSafeEncode($currentPrivateKeyDetails["rsa"]["e"])
],
'newKey' => [
"kty" => "RSA",
"n" => Utilities\Base64::UrlSafeEncode($newPrivateKeyDetails["rsa"]["n"]),
"e" => Utilities\Base64::UrlSafeEncode($newPrivateKeyDetails["rsa"]["e"])
]
];
$outerPayload = Utilities\RequestSigner::JWK(
$innerPayload,
Cache\DirectoryResponse::getInstance()->get()->getKeyChange(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath(),
'private-replacement.pem'
);
$data = Utilities\RequestSigner::KID(
$outerPayload,
Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
Cache\DirectoryResponse::getInstance()->get()->getKeyChange(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath(),
'private.pem'
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
Cache\DirectoryResponse::getInstance()->get()->getKeyChange(),
$data
);
return new Response\Account\ChangeKeys($result);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Utilities;
use LE_ACME2\Exception;
use LE_ACME2\Account;
class Create extends AbstractRequest {
protected $_account;
public function __construct(Account $account) {
$this->_account = $account;
}
/**
* @return Response\AbstractResponse|Response\Account\Create
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$payload = [
'contact' => $this->_buildContactPayload($this->_account->getEmail()),
'termsOfServiceAgreed' => true,
];
$jwk = Utilities\RequestSigner::JWKString(
$payload,
Cache\DirectoryResponse::getInstance()->get()->getNewAccount(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
Cache\DirectoryResponse::getInstance()->get()->getNewAccount(),
$jwk
);
return new Response\Account\Create($result);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Response;
use LE_ACME2\Exception;
class Deactivate extends AbstractLocation {
protected function _getPayload() : array {
return [
'status' => 'deactivated',
];
}
/**
* @return Response\AbstractResponse|Response\Account\Deactivate
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
return new Response\Account\Deactivate($this->_getRawResponse());
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
use LE_ACME2\Account;
class Get extends AbstractRequest {
protected $_account;
public function __construct(Account $account) {
$this->_account = $account;
}
/**
* @return Response\AbstractResponse|Response\Account\Get
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$payload = [
'onlyReturnExisting' => true,
];
$jwk = Utilities\RequestSigner::JWKString(
$payload,
Cache\DirectoryResponse::getInstance()->get()->getNewAccount(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
Cache\DirectoryResponse::getInstance()->get()->getNewAccount(),
$jwk
);
return new Response\Account\Get($result);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Response;
use LE_ACME2\Exception;
class GetData extends AbstractLocation {
protected function _getPayload() : array {
return [];
}
/**
* @return Response\AbstractResponse|Response\Account\GetData
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
return new Response\Account\GetData($this->_getRawResponse());
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace LE_ACME2\Request\Account;
defined('ABSPATH') or die();
use LE_ACME2\Response;
use LE_ACME2\Exception;
use LE_ACME2\Account;
class Update extends AbstractLocation {
protected $_newEmail;
public function __construct(Account $account, $newEmail) {
parent::__construct($account);
$this->_newEmail = $newEmail;
}
protected function _getPayload() : array {
return [
'contact' => $this->_buildContactPayload($this->_newEmail),
];
}
/**
* @return Response\AbstractResponse|Response\Account\Update
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
return new Response\Account\Update($this->_getRawResponse());
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace LE_ACME2\Request\Authorization;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Response;
use LE_ACME2\Utilities;
use LE_ACME2\Account;
class Get extends AbstractRequest {
protected $_account;
protected $_authorizationURL;
public function __construct(Account $account, string $authorizationURL) {
$this->_account = $account;
$this->_authorizationURL = $authorizationURL;
}
/**
* @return Response\AbstractResponse|Response\Authorization\Get
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
public function getResponse() : Response\AbstractResponse {
$kid = Utilities\RequestSigner::KID(
null,
Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
$this->_authorizationURL,
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
$this->_authorizationURL,
$kid
);
return new Response\Authorization\Get($result);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace LE_ACME2\Request\Authorization;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Response;
use LE_ACME2\Struct\ChallengeAuthorizationKey;
use LE_ACME2\Utilities;
use LE_ACME2\Account;
use LE_ACME2\Order;
class Start extends AbstractRequest {
protected $_account;
protected $_order;
protected $_challenge;
public function __construct(Account $account, Order $order, Response\Authorization\Struct\Challenge $challenge) {
$this->_account = $account;
$this->_order = $order;
$this->_challenge = $challenge;
}
/**
* @return Response\AbstractResponse|Response\Authorization\Start
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
public function getResponse() : Response\AbstractResponse {
$payload = [
'keyAuthorization' => (new ChallengeAuthorizationKey($this->_account))->get($this->_challenge->token)
];
$kid = Utilities\RequestSigner::KID(
$payload,
Cache\AccountResponse::getInstance()->get($this->_account)->getLocation(),
$this->_challenge->url,
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_account->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
$this->_challenge->url,
$kid
);
return new Response\Authorization\Start($result);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace LE_ACME2\Request;
defined('ABSPATH') or die();
use LE_ACME2\Response;
use LE_ACME2\Connector\Connector;
use LE_ACME2\Exception;
class GetDirectory extends AbstractRequest {
/**
* @return Response\AbstractResponse|Response\GetDirectory
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$connector = Connector::getInstance();
$result = $connector->request(
Connector::METHOD_GET,
$connector->getBaseURL() . '/directory'
);
return new Response\GetDirectory($result);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace LE_ACME2\Request;
defined('ABSPATH') or die();
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
class GetNewNonce extends AbstractRequest {
/**
* @return Response\AbstractResponse|Response\GetNewNonce
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_HEAD,
Cache\DirectoryResponse::getInstance()->get()->getNewNonce()
);
return new Response\GetNewNonce($result);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace LE_ACME2\Request\Order;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
use LE_ACME2\Order;
class Create extends AbstractRequest {
protected $_order;
public function __construct(Order $order) {
$this->_order = $order;
}
/**
* @return Response\AbstractResponse|Response\Order\Create
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$identifiers = [];
foreach($this->_order->getSubjects() as $subject) {
$identifiers[] = [
'type' => 'dns',
'value' => $subject
];
}
$payload = [
'identifiers' => $identifiers,
'notBefore' => '',
'notAfter' => '',
];
$kid = Utilities\RequestSigner::KID(
$payload,
Cache\AccountResponse::getInstance()->get($this->_order->getAccount())->getLocation(),
Cache\DirectoryResponse::getInstance()->get()->getNewOrder(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_order->getAccount()->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
Cache\DirectoryResponse::getInstance()->get()->getNewOrder(),
$kid
);
return new Response\Order\Create($result);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace LE_ACME2\Request\Order;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
use LE_ACME2\Order;
class Finalize extends AbstractRequest {
protected $_order;
protected $_orderResponse;
public function __construct(Order $order, Response\Order\AbstractOrder $orderResponse) {
$this->_order = $order;
$this->_orderResponse = $orderResponse;
}
/**
* @return Response\AbstractResponse|Response\Order\Finalize
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$csr = Utilities\Certificate::generateCSR($this->_order);
if(preg_match('~-----BEGIN\sCERTIFICATE\sREQUEST-----(.*)-----END\sCERTIFICATE\sREQUEST-----~s', $csr, $matches))
$csr = $matches[1];
$csr = trim(Utilities\Base64::UrlSafeEncode(base64_decode($csr)));
$payload = [
'csr' => $csr
];
$kid = Utilities\RequestSigner::KID(
$payload,
Cache\AccountResponse::getInstance()->get($this->_order->getAccount())->getLocation(),
$this->_orderResponse->getFinalize(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_order->getAccount()->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
$this->_orderResponse->getFinalize(),
$kid
);
return new Response\Order\Finalize($result);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace LE_ACME2\Request\Order;
defined('ABSPATH') or die();
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
use LE_ACME2\Order;
class Get extends AbstractRequest {
protected $_order;
protected $_orderResponse;
public function __construct(Order $order, Response\Order\AbstractOrder $orderResponse) {
$this->_order = $order;
$this->_orderResponse = $orderResponse;
}
/**
* @return Response\AbstractResponse|Response\Order\Get
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$kid = Utilities\RequestSigner::KID(
null,
Cache\AccountResponse::getInstance()->get($this->_order->getAccount())->getLocation(),
$this->_orderResponse->getLocation(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_order->getAccount()->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
$this->_orderResponse->getLocation(),
$kid
);
return new Response\Order\Get($result, $this->_orderResponse->getLocation());
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace LE_ACME2\Request\Order;
defined('ABSPATH') or die();
use LE_ACME2\Order;
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Response;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Utilities;
class GetCertificate extends AbstractRequest {
protected $_order;
protected $_orderResponse;
private $_alternativeUrl = null;
public function __construct(Order $order, Response\Order\AbstractOrder $orderResponse,
string $alternativeUrl = null
) {
$this->_order = $order;
$this->_orderResponse = $orderResponse;
if($alternativeUrl !== null) {
$this->_alternativeUrl = $alternativeUrl;
}
}
/**
* @return Response\AbstractResponse|Response\Order\GetCertificate
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$url = $this->_alternativeUrl === null ?
$this->_orderResponse->getCertificate() :
$this->_alternativeUrl;
$kid = Utilities\RequestSigner::KID(
null,
Cache\AccountResponse::getInstance()->get($this->_order->getAccount())->getLocation(),
$url,
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_order->getAccount()->getKeyDirectoryPath()
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
$url,
$kid
);
return new Response\Order\GetCertificate($result);
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace LE_ACME2\Request\Order;
defined('ABSPATH') or die();
use LE_ACME2\Response;
use LE_ACME2\Request\AbstractRequest;
use LE_ACME2\Connector;
use LE_ACME2\Cache;
use LE_ACME2\Exception;
use LE_ACME2\Struct;
use LE_ACME2\Utilities;
class RevokeCertificate extends AbstractRequest {
protected $_certificateBundle;
protected $_reason;
public function __construct(Struct\CertificateBundle $certificateBundle, $reason) {
$this->_certificateBundle = $certificateBundle;
$this->_reason = $reason;
}
/**
* @return Response\AbstractResponse|Response\Order\RevokeCertificate
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function getResponse() : Response\AbstractResponse {
$certificate = file_get_contents($this->_certificateBundle->path . $this->_certificateBundle->certificate);
preg_match('~-----BEGIN\sCERTIFICATE-----(.*)-----END\sCERTIFICATE-----~s', $certificate, $matches);
$certificate = trim(Utilities\Base64::UrlSafeEncode(base64_decode(trim($matches[1]))));
$payload = [
'certificate' => $certificate,
'reason' => $this->_reason
];
$jwk = Utilities\RequestSigner::JWKString(
$payload,
Cache\DirectoryResponse::getInstance()->get()->getRevokeCert(),
Cache\NewNonceResponse::getInstance()->get()->getNonce(),
$this->_certificateBundle->path,
$this->_certificateBundle->private
);
$result = Connector\Connector::getInstance()->request(
Connector\Connector::METHOD_POST,
Cache\DirectoryResponse::getInstance()->get()->getRevokeCert(),
$jwk
);
return new Response\Order\RevokeCertificate($result);
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace LE_ACME2\Response;
defined('ABSPATH') or die();
use LE_ACME2\Exception;
use LE_ACME2\Connector\RawResponse;
abstract class AbstractResponse {
protected $_raw = NULL;
protected $_pattern_header_location = '/^Location: (\S+)$/i';
/**
* AbstractResponse constructor.
*
* @param RawResponse $raw
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function __construct(RawResponse $raw) {
$this->_raw = $raw;
if($this->_isRateLimitReached()) {
$detail = "";
if(isset($raw->body['type']) && $raw->body['type'] == 'urn:ietf:params:acme:error:rateLimited') {
$detail = $raw->body['detail'];
}
throw new Exception\RateLimitReached($raw->request, $detail);
}
$result = $this->_isValid();
if(!$result) {
$responseStatus = $this->_preg_match_headerLine('/^HTTP\/.* [0-9]{3,} /i');
throw new Exception\InvalidResponse(
$raw,
$responseStatus ? $responseStatus[1] : null
);
}
}
protected function _preg_match_headerLine(string $pattern) : ?array {
foreach($this->_raw->header as $line) {
if(preg_match($pattern, $line, $matches) === 1)
return $matches;
}
return null;
}
protected function _isRateLimitReached() : bool {
return $this->_preg_match_headerLine('/^HTTP\/.* 429/i') !== null;
}
protected function _isValid() : bool {
return $this->_preg_match_headerLine('/^HTTP\/.* 201/i') !== null || //Created
$this->_preg_match_headerLine('/^HTTP\/.* 200/i') !== null ||
$this->_preg_match_headerLine('/^HTTP\/.* 204/i') !== null;
}
public function getRaw() : RawResponse {
return $this->_raw;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
abstract class AbstractAccount extends AbstractResponse {
const STATUS_VALID = 'valid';
public function getLocation() : string {
$matches = $this->_preg_match_headerLine($this->_pattern_header_location);
return trim($matches[1]);
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
abstract class AbstractLocation extends AbstractResponse {
public function getKey() : string {
return $this->_raw->body['key'];
}
public function getContact() : string {
return $this->_raw->body['contact'];
}
public function getAgreement() : string {
return $this->_raw->body['agreement'];
}
public function getInitialIP() : string {
return $this->_raw->body['initialIp'];
}
public function getCreatedAt() : string {
return $this->_raw->body['createdAt'];
}
public function getStatus() : string {
return $this->_raw->body['status'];
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
class ChangeKeys extends AbstractResponse {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
class Create extends AbstractAccount {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
class Deactivate extends AbstractLocation {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
class Get extends AbstractAccount {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
class GetData extends AbstractLocation {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Account;
defined('ABSPATH') or die();
class Update extends AbstractLocation {}

View File

@@ -0,0 +1,36 @@
<?php
namespace LE_ACME2\Response\Authorization;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
use LE_ACME2\Connector\RawResponse;
use LE_ACME2\Exception;
class AbstractAuthorization extends AbstractResponse {
/**
* AbstractAuthorization constructor.
* @param RawResponse $raw
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
* @throws Exception\ExpiredAuthorization
*/
public function __construct(RawResponse $raw) {
parent::__construct($raw);
}
/**
* @return bool
* @throws Exception\ExpiredAuthorization
*/
protected function _isValid() : bool {
if($this->_preg_match_headerLine('/^HTTP\/.* 404/i') !== null) {
throw new Exception\ExpiredAuthorization();
}
return parent::_isValid();
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace LE_ACME2\Response\Authorization;
defined('ABSPATH') or die();
use LE_ACME2\Response\Authorization\Struct;
class Get extends AbstractAuthorization {
public function getIdentifier() : Struct\Identifier {
return new Struct\Identifier($this->_raw->body['identifier']['type'], $this->_raw->body['identifier']['value']);
}
public function getStatus() : string {
return $this->_raw->body['status'];
}
public function getExpires() : string {
return $this->_raw->body['expires'];
}
public function getChallenges() : array {
return $this->_raw->body['challenges'];
}
/**
* @param $type
* @return Struct\Challenge
*/
public function getChallenge(string $type) : Struct\Challenge {
foreach($this->getChallenges() as $challenge) {
if($type == $challenge['type'])
return new Struct\Challenge($challenge['type'], $challenge['status'], $challenge['url'], $challenge['token']);
}
throw new \RuntimeException('No challenge found with given type');
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Authorization;
defined('ABSPATH') or die();
class Start extends AbstractAuthorization {}

View File

@@ -0,0 +1,25 @@
<?php
namespace LE_ACME2\Response\Authorization\Struct;
defined('ABSPATH') or die();
class Challenge {
const STATUS_PROGRESSING = 'processing';
const STATUS_PENDING = 'pending';
const STATUS_VALID = 'valid';
const STATUS_INVALID = 'invalid';
public $type;
public $status;
public $url;
public $token;
public function __construct(string $type, string $status, string $url, string $token) {
$this->type = $type;
$this->status = $status;
$this->url = $url;
$this->token = $token;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace LE_ACME2\Response\Authorization\Struct;
defined('ABSPATH') or die();
class Identifier {
public $type;
public $value;
public function __construct(string $type, string $value) {
$this->type = $type;
$this->value = $value;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace LE_ACME2\Response;
defined('ABSPATH') or die();
class GetDirectory extends AbstractResponse {
public function getKeyChange() : string {
return $this->_raw->body['keyChange'];
}
public function getNewAccount() : string {
return $this->_raw->body['newAccount'];
}
public function getNewNonce() : string {
return $this->_raw->body['newNonce'];
}
public function getNewOrder() : string {
return $this->_raw->body['newOrder'];
}
public function getRevokeCert() : string {
return $this->_raw->body['revokeCert'];
}
public function getTermsOfService() : string {
return $this->_raw->body['meta']['termsOfService'];
}
public function getWebsite() : string {
return $this->_raw->body['meta']['website'];
}
public function getCaaIdentities() : string {
return $this->_raw->body['meta']['caaIdentities'];
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace LE_ACME2\Response;
defined('ABSPATH') or die();
class GetNewNonce extends AbstractResponse {
protected $_pattern = '/^Replay\-Nonce: (\S+)$/i';
protected function _isValid() : bool {
return $this->_preg_match_headerLine($this->_pattern) !== null;
}
public function getNonce() : string {
$matches = $this->_preg_match_headerLine($this->_pattern);
return trim($matches[1]);
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace LE_ACME2\Response\Order;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
use LE_ACME2\Exception;
abstract class AbstractOrder extends AbstractResponse {
const STATUS_PENDING = 'pending';
const STATUS_VALID = 'valid';
const STATUS_READY = 'ready';
const STATUS_INVALID = 'invalid';
public function getLocation() : string {
$matches = $this->_preg_match_headerLine($this->_pattern_header_location);
return trim($matches[1]);
}
public function getStatus() : string {
return $this->_raw->body['status'];
}
public function getExpires() : string {
return $this->_raw->body['expires'];
}
public function getIdentifiers() : array {
return $this->_raw->body['identifiers'];
}
public function getAuthorizations() : array {
return $this->_raw->body['authorizations'];
}
public function getFinalize() : string {
return $this->_raw->body['finalize'];
}
public function getCertificate() : string {
return $this->_raw->body['certificate'];
}
/**
* @return bool
* @throws Exception\StatusInvalid
*/
protected function _isValid(): bool {
if(!parent::_isValid()) {
return false;
}
if(
$this->getStatus() == AbstractOrder::STATUS_INVALID
) {
throw new Exception\StatusInvalid('Order has status "' . AbstractOrder::STATUS_INVALID . '"'.
'. Probably all authorizations have failed. ' . PHP_EOL .
'Please see: ' . $this->getLocation() . PHP_EOL .
'Continue by using $order->clear() after getting rid of the problem'
);
}
return true;
}
}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Order;
defined('ABSPATH') or die();
class Create extends AbstractOrder {}

View File

@@ -0,0 +1,6 @@
<?php
namespace LE_ACME2\Response\Order;
defined('ABSPATH') or die();
class Finalize extends AbstractOrder {}

View File

@@ -0,0 +1,27 @@
<?php
namespace LE_ACME2\Response\Order;
defined('ABSPATH') or die();
use LE_ACME2\Connector\RawResponse;
use LE_ACME2\Exception;
class Get extends AbstractOrder {
/**
* Get constructor.
*
* @param RawResponse $raw
* @param $orderURL
* @throws Exception\InvalidResponse
* @throws Exception\RateLimitReached
*/
public function __construct(RawResponse $raw, string $orderURL) {
// Dirty fix: Header of response "Get" does not contain an order url, instead of response "Create"
// Is needed on production server, not on staging server - tested: 12.04.2021
$raw->header[] = 'Location: ' . $orderURL;
parent::__construct($raw);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace LE_ACME2\Response\Order;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
class GetCertificate extends AbstractResponse {
protected $_pattern = '~(-----BEGIN\sCERTIFICATE-----[\s\S]+?-----END\sCERTIFICATE-----)~i';
public function getCertificate() : string {
if(preg_match_all($this->_pattern, $this->_raw->body, $matches)) {
return $matches[0][0];
}
throw new \RuntimeException('Preg_match_all has returned false - invalid pattern?');
}
public function getIntermediate() : string {
if(preg_match_all($this->_pattern, $this->_raw->body, $matches)) {
$result = '';
for($i=1; $i<count($matches[0]); $i++) {
$result .= "\n" . $matches[0][$i];
}
return $result;
}
throw new \RuntimeException('Preg_match_all has returned false - invalid pattern?');
}
/**
* @return string[]
*/
public function getAlternativeLinks() : array {
$result = [];
foreach($this->_raw->header as $line) {
$matches = [];
preg_match_all('/^link: <(.*)>;rel="alternate"$/', $line, $matches);
if(isset($matches[1][0])) {
$result[] = $matches[1][0];
}
}
return $result;
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace LE_ACME2\Response\Order;
defined('ABSPATH') or die();
use LE_ACME2\Response\AbstractResponse;
class RevokeCertificate extends AbstractResponse {}

View File

@@ -0,0 +1,19 @@
<?php
namespace LE_ACME2;
defined('ABSPATH') or die();
trait SingletonTrait {
private static $_instance = NULL;
/**
* @return static
*/
final public static function getInstance(): self {
if( self::$_instance === NULL ) {
self::$_instance = new self();
}
return self::$_instance;
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace LE_ACME2\Struct;
defined('ABSPATH') or die();
class CertificateBundle {
public $path;
public $private;
public $certificate;
public $intermediate;
public $expireTime;
public function __construct(string $path, string $private, string $certificate, string $intermediate, int $expireTime) {
$this->path = $path;
$this->private = $private;
$this->certificate = $certificate;
$this->intermediate = $intermediate;
$this->expireTime = $expireTime;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace LE_ACME2\Struct;
defined('ABSPATH') or die();
use LE_ACME2\Account;
use LE_ACME2\Utilities;
class ChallengeAuthorizationKey {
private $_account;
public function __construct(Account $account) {
$this->_account = $account;
}
public function get(string $token) : string {
return $token . '.' . $this->_getDigest();
}
public function getEncoded(string $token) : string {
return Utilities\Base64::UrlSafeEncode(
hash('sha256', $this->get($token), true)
);
}
private function _getDigest() : string {
$privateKey = openssl_pkey_get_private(file_get_contents($this->_account->getKeyDirectoryPath() . 'private.pem'));
$details = openssl_pkey_get_details($privateKey);
$header = array(
"e" => Utilities\Base64::UrlSafeEncode($details["rsa"]["e"]),
"kty" => "RSA",
"n" => Utilities\Base64::UrlSafeEncode($details["rsa"]["n"])
);
return Utilities\Base64::UrlSafeEncode(hash('sha256', json_encode($header), true));
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace LE_ACME2\Utilities;
defined('ABSPATH') or die();
class Base64 {
/**
* Encodes a string input to a base64 encoded string which is URL safe.
*
* @param string $input The input string to encode.
* @return string Returns a URL safe base64 encoded string.
*/
public static function UrlSafeEncode(string $input) : string {
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
}
/**
* Decodes a string that is URL safe base64 encoded.
*
* @param string $input The encoded input string to decode.
* @return string Returns the decoded input string.
*/
public static function UrlSafeDecode(string $input) : string {
$remainder = strlen($input) % 4;
if ($remainder) {
$padlen = 4 - $remainder;
$input .= str_repeat('=', $padlen);
}
return base64_decode(strtr($input, '-_', '+/'));
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace LE_ACME2\Utilities;
defined('ABSPATH') or die();
use LE_ACME2\Order;
use LE_ACME2\Exception\OpenSSLException;
class Certificate {
protected static $_featureOCSPMustStapleEnabled = false;
public static function enableFeatureOCSPMustStaple() {
self::$_featureOCSPMustStapleEnabled = true;
}
public static function disableFeatureOCSPMustStaple() {
self::$_featureOCSPMustStapleEnabled = false;
}
/**
* @param Order $order
* @return string
* @throws OpenSSLException
*/
public static function generateCSR(Order $order) : string {
$dn = [
"commonName" => $order->getSubjects()[0]
];
$san = implode(",", array_map(function ($dns) {
return "DNS:" . $dns;
}, $order->getSubjects())
);
$configFilePath = $order->getKeyDirectoryPath() . 'csr_config';
$config = 'HOME = .
RANDFILE = ' . $order->getKeyDirectoryPath() . '.rnd
[ req ]
default_bits = 4096
default_keyfile = privkey.pem
distinguished_name = req_distinguished_name
req_extensions = v3_req
[ req_distinguished_name ]
countryName = Country Name (2 letter code)
[ v3_req ]
basicConstraints = CA:FALSE
subjectAltName = ' . $san . '
keyUsage = nonRepudiation, digitalSignature, keyEncipherment';
if(self::$_featureOCSPMustStapleEnabled) {
$config .= PHP_EOL . 'tlsfeature=status_request';
}
file_put_contents($configFilePath, $config);
$privateKey = openssl_pkey_get_private(
file_get_contents($order->getKeyDirectoryPath() . 'private.pem')
);
if($privateKey === false) {
throw new OpenSSLException('openssl_pkey_get_private');
}
$csr = openssl_csr_new(
$dn,
$privateKey,
[
'config' => $configFilePath,
'digest_alg' => 'sha256'
]
);
if($csr === false) {
throw new OpenSSLException('openssl_csr_new');
}
if(!openssl_csr_export($csr, $csr)) {
throw new OpenSSLException('openssl_csr_export');
}
unlink($configFilePath);
return $csr;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace LE_ACME2\Utilities;
defined('ABSPATH') or die();
class KeyGenerator {
/**
* Generates a new RSA keypair and saves both keys to a new file.
*
* @param string $directory The directory in which to store the new keys.
* @param string $privateKeyFile The filename for the private key file.
* @param string $publicKeyFile The filename for the public key file.
*/
public static function RSA(string $directory, string $privateKeyFile = 'private.pem', string $publicKeyFile = 'public.pem') {
$res = openssl_pkey_new([
"private_key_type" => OPENSSL_KEYTYPE_RSA,
"private_key_bits" => 4096,
]);
if(!openssl_pkey_export($res, $privateKey))
throw new \RuntimeException("RSA keypair export failed!");
$details = openssl_pkey_get_details($res);
file_put_contents($directory . $privateKeyFile, $privateKey);
file_put_contents($directory . $publicKeyFile, $details['key']);
if(PHP_MAJOR_VERSION < 8) {
// deprecated after PHP 8.0.0 and not needed anymore
openssl_pkey_free($res);
}
}
/**
* Generates a new EC prime256v1 keypair and saves both keys to a new file.
*
* @param string $directory The directory in which to store the new keys.
* @param string $privateKeyFile The filename for the private key file.
* @param string $publicKeyFile The filename for the public key file.
*/
public static function EC(string $directory, string $privateKeyFile = 'private.pem', string $publicKeyFile = 'public.pem') {
if (version_compare(PHP_VERSION, '7.1.0') == -1)
throw new \RuntimeException("PHP 7.1+ required for EC keys");
$res = openssl_pkey_new([
"private_key_type" => OPENSSL_KEYTYPE_EC,
"curve_name" => "prime256v1",
]);
if(!openssl_pkey_export($res, $privateKey))
throw new \RuntimeException("EC keypair export failed!");
$details = openssl_pkey_get_details($res);
file_put_contents($directory . $privateKeyFile, $privateKey);
file_put_contents($directory . $publicKeyFile, $details['key']);
if(PHP_MAJOR_VERSION < 8) {
// deprecated after PHP 8.0.0 and not needed anymore
openssl_pkey_free($res);
}
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace LE_ACME2\Utilities;
defined('ABSPATH') or die();
use LE_ACME2\SingletonTrait;
class Logger {
use SingletonTrait;
const LEVEL_DISABLED = 0;
const LEVEL_INFO = 1;
const LEVEL_DEBUG = 2;
private function __construct() {}
protected $_desiredLevel = self::LEVEL_DISABLED;
public function setDesiredLevel(int $desiredLevel) {
$this->_desiredLevel = $desiredLevel;
}
/**
* @param int $level
* @param string $message
* @param string|array|object $data
*/
public function add(int $level, string $message, $data = array()) {
if($level > $this->_desiredLevel)
return;
$e = new \Exception();
$trace = $e->getTrace();
unset($trace[0]);
$output = '<b>' . date('d-m-Y H:i:s') . ': ' . $message . '</b><br>' . "\n";
if($this->_desiredLevel == self::LEVEL_DEBUG) {
$step = 0;
foreach ($trace as $traceItem) {
if(!isset($traceItem['class']) || !isset($traceItem['function'])) {
continue;
}
$output .= 'Trace #' . $step . ': ' . $traceItem['class'] . '::' . $traceItem['function'] . '<br/>' . "\n";
$step++;
}
if ((is_array($data) && count($data) > 0) || !is_array($data))
$output .= "\n" .'<br/>Data:<br/>' . "\n" . '<pre>' . var_export($data, true) . '</pre>';
$output .= '<br><br>' . "\n\n";
}
if(PHP_SAPI == 'cli') {
$output = strip_tags($output);
}
echo $output;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace LE_ACME2\Utilities;
defined('ABSPATH') or die();
class RequestSigner {
/**
* Generates a JSON Web Key signature to attach to the request.
*
* @param array $payload The payload to add to the signature.
* @param string $url The URL to use in the signature.
* @param string $nonce
* @param string $privateKeyDir The directory to get the private key from. Default to the account keys directory given in the constructor. (optional)
* @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. (optional)
*
* @return array Returns an array containing the signature.
*/
public static function JWK(array $payload, string $url, string $nonce, string $privateKeyDir, string $privateKeyFile = 'private.pem') : array {
Logger::getInstance()->add(Logger::LEVEL_DEBUG, 'JWK sign request for ' . $url, $payload);
$privateKey = openssl_pkey_get_private(file_get_contents($privateKeyDir . $privateKeyFile));
$details = openssl_pkey_get_details($privateKey);
$protected = [
"alg" => "RS256",
"jwk" => [
"kty" => "RSA",
"n" => Base64::UrlSafeEncode($details["rsa"]["n"]),
"e" => Base64::UrlSafeEncode($details["rsa"]["e"]),
],
"nonce" => $nonce,
"url" => $url
];
$payload64 = Base64::UrlSafeEncode(str_replace('\\/', '/', json_encode($payload)));
$protected64 = Base64::UrlSafeEncode(json_encode($protected));
openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
$signed64 = Base64::UrlSafeEncode($signed);
$data = array(
'protected' => $protected64,
'payload' => $payload64,
'signature' => $signed64
);
return $data;
}
/**
* Generates a JSON Web Key signature to attach to the request.
*
* @param array $payload The payload to add to the signature.
* @param string $url The URL to use in the signature.
* @param string $nonce
* @param string $privateKeyDir The directory to get the private key from. Default to the account keys directory given in the constructor. (optional)
* @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. (optional)
*
* @return string Returns a JSON encoded string containing the signature.
*/
public static function JWKString(array $payload, string $url, string $nonce, string $privateKeyDir, string $privateKeyFile = 'private.pem') : string {
$jwk = self::JWK($payload, $url, $nonce, $privateKeyDir, $privateKeyFile);
return json_encode($jwk);
}
/**
* Generates a Key ID signature to attach to the request.
*
* @param array|null $payload The payload to add to the signature.
* @param string $kid The Key ID to use in the signature.
* @param string $url The URL to use in the signature.
* @param string $nonce
* @param string $privateKeyDir The directory to get the private key from.
* @param string $privateKeyFile The private key to sign the request with. Defaults to 'private.pem'. (optional)
*
* @return string Returns a JSON encoded string containing the signature.
*/
public static function KID(?array $payload, string $kid, string $url, string $nonce, string $privateKeyDir, string $privateKeyFile = 'private.pem') : string {
Logger::getInstance()->add(Logger::LEVEL_DEBUG, 'KID sign request for ' . $url, $payload);
$privateKey = openssl_pkey_get_private(file_get_contents($privateKeyDir . $privateKeyFile));
// TODO: unused - $details = openssl_pkey_get_details($privateKey);
$protected = [
"alg" => "RS256",
"kid" => $kid,
"nonce" => $nonce,
"url" => $url
];
Logger::getInstance()->add(Logger::LEVEL_DEBUG, 'KID: ready to sign request for: ' . $url, $protected);
$payload = $payload === null ? "" : str_replace('\\/', '/', json_encode($payload));
$payload64 = Base64::UrlSafeEncode($payload);
$protected64 = Base64::UrlSafeEncode(json_encode($protected));
openssl_sign($protected64.'.'.$payload64, $signed, $privateKey, "SHA256");
$signed64 = Base64::UrlSafeEncode($signed);
$data = [
'protected' => $protected64,
'payload' => $payload64,
'signature' => $signed64
];
return json_encode($data);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace LE_ACME2Tests;
defined('ABSPATH') or die();
use PHPUnit\Framework\TestCase;
use LE_ACME2;
abstract class AbstractTest extends TestCase {
public function __construct() {
parent::__construct();
LE_ACME2\Connector\Connector::getInstance()->useStagingServer(true);
}
}

View File

@@ -0,0 +1,177 @@
<?php
namespace LE_ACME2Tests;
defined('ABSPATH') or die();
use LE_ACME2\Exception\InvalidResponse;
/**
* @covers \LE_ACME2\Account
*/
class AccountTest extends AbstractTest {
private $_commonKeyDirectoryPath;
private $_email;
public function __construct() {
parent::__construct();
$this->_commonKeyDirectoryPath = TestHelper::getInstance()->getTempPath() . 'le-storage/';
$this->_email = 'le_acme2_php_client@test.com';
}
public function testNonExistingCommonKeyDirectoryPath() {
$this->assertTrue(\LE_ACME2\Account::getCommonKeyDirectoryPath() === null);
$notExistingPath = TestHelper::getInstance()->getTempPath() . 'should-not-exist/';
$this->expectException(\RuntimeException::class);
\LE_ACME2\Account::setCommonKeyDirectoryPath($notExistingPath);
}
public function testCommonKeyDirectoryPath() {
if(!file_exists($this->_commonKeyDirectoryPath)) {
mkdir($this->_commonKeyDirectoryPath);
}
\LE_ACME2\Account::setCommonKeyDirectoryPath($this->_commonKeyDirectoryPath);
$this->assertTrue(
\LE_ACME2\Account::getCommonKeyDirectoryPath() === $this->_commonKeyDirectoryPath
);
}
public function testNonExisting() {
if(\LE_ACME2\Account::exists($this->_email)) {
$this->markTestSkipped('Skipped: Account does already exist');
}
$this->assertTrue(!\LE_ACME2\Account::exists($this->_email));
$this->expectException(\RuntimeException::class);
\LE_ACME2\Account::get($this->_email);
}
public function testCreate() {
if(\LE_ACME2\Account::exists($this->_email)) {
// Skipping account modification tests, when the account already exists
// to reduce the LE api usage while developing
TestHelper::getInstance()->setSkipAccountModificationTests(true);
$this->markTestSkipped('Account modifications skipped: Account does already exist');
}
$this->assertTrue(!\LE_ACME2\Account::exists($this->_email));
$account = \LE_ACME2\Account::create($this->_email);
$this->assertTrue(is_object($account));
$this->assertTrue($account->getEmail() === $this->_email);
$account = \LE_ACME2\Account::get($this->_email);
$this->assertTrue(is_object($account));
$result = $account->getData();
$this->assertTrue($result->getStatus() === \LE_ACME2\Response\Account\AbstractAccount::STATUS_VALID);
}
public function testInvalidCreate() {
if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
$this->expectNotToPerformAssertions();
return;
}
$this->expectException(InvalidResponse::class);
$this->expectExceptionMessage(
'Invalid response received: ' .
'urn:ietf:params:acme:error:invalidEmail' .
' - ' .
'Error creating new account :: invalid contact domain. Contact emails @example.org are forbidden'
);
\LE_ACME2\Account::create('test@example.org');
}
public function testModification() {
if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
$this->expectNotToPerformAssertions();
return;
}
$account = \LE_ACME2\Account::get($this->_email);
$this->assertTrue(is_object($account));
$keyDirectoryPath = $account->getKeyDirectoryPath();
$newEmail = 'new-' . $this->_email;
// An email from example.org is not allowed
$result = $account->update('test@example.org');
$this->assertTrue($result === false);
$result = $account->update($newEmail);
$this->assertTrue($result === true);
$this->assertTrue($account->getKeyDirectoryPath() !== $keyDirectoryPath);
$this->assertTrue(file_exists($account->getKeyDirectoryPath()));
$result = $account->update($this->_email);
$this->assertTrue($result === true);
$result = $account->changeKeys();
$this->assertTrue($result === true);
}
public function testDeactivation() {
if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
$this->expectNotToPerformAssertions();
return;
}
$account = \LE_ACME2\Account::get($this->_email);
$this->assertTrue(is_object($account));
$result = $account->deactivate();
$this->assertTrue($result === true);
// The account is already deactivated
$result = $account->deactivate();
$this->assertTrue($result === false);
// The account is already deactivated
$result = $account->changeKeys();
$this->assertTrue($result === false);
// The account is already deactivated
$this->expectException(\LE_ACME2\Exception\InvalidResponse::class);
$account->getData();
}
public function testCreationAfterDeactivation() {
if(TestHelper::getInstance()->shouldSkipAccountModificationTests()) {
$this->expectNotToPerformAssertions();
return;
}
$account = \LE_ACME2\Account::get($this->_email);
$this->assertTrue(is_object($account));
system('rm -R ' . $account->getKeyDirectoryPath());
$this->assertTrue(!\LE_ACME2\Account::exists($this->_email));
$account = \LE_ACME2\Account::create($this->_email);
$this->assertTrue(is_object($account));
}
public function test() {
$account = \LE_ACME2\Account::get($this->_email);
$this->assertTrue(is_object($account));
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace LE_ACME2Tests\Authorizer;
defined('ABSPATH') or die();
use LE_ACME2Tests\AbstractTest;
use LE_ACME2Tests\TestHelper;
/**
* @covers \LE_ACME2\Authorizer\HTTP
*/
class HTTPTest extends AbstractTest {
private $_directoryPath;
public function __construct() {
parent::__construct();
$this->_directoryPath = TestHelper::getInstance()->getTempPath() . 'acme-challenges/';
}
public function testNonExistingDirectoryPath() {
$this->assertTrue(\LE_ACME2\Authorizer\HTTP::getDirectoryPath() === null);
$this->expectException(\RuntimeException::class);
\LE_ACME2\Authorizer\HTTP::setDirectoryPath(TestHelper::getInstance()->getNonExistingPath());
}
public function testDirectoryPath() {
if(!file_exists($this->_directoryPath)) {
mkdir($this->_directoryPath);
}
\LE_ACME2\Authorizer\HTTP::setDirectoryPath($this->_directoryPath);
$this->assertTrue(
\LE_ACME2\Authorizer\HTTP::getDirectoryPath() === $this->_directoryPath
);
}
}

View File

@@ -0,0 +1,42 @@
<?php
namespace LE_ACME2Tests;
defined('ABSPATH') or die();
use LE_ACME2\SingletonTrait;
class TestHelper {
private $_tempPath;
private $_nonExistingPath;
use SingletonTrait;
private function __construct() {
$projectPath = realpath($_SERVER[ 'PWD' ]) . '/';
$this->_tempPath = $projectPath . 'temp/';
if( !file_exists($this->_tempPath) ) {
mkdir($this->_tempPath);
}
$this->_nonExistingPath = $this->getTempPath() . 'should-not-exist/';
}
public function getTempPath() : string {
return $this->_tempPath;
}
public function getNonExistingPath() : string {
return $this->_nonExistingPath;
}
private $_skipAccountModificationTests = false;
public function setSkipAccountModificationTests(bool $value) : void {
$this->_skipAccountModificationTests = $value;
}
public function shouldSkipAccountModificationTests() : bool {
return $this->_skipAccountModificationTests;
}
}

View File

@@ -0,0 +1 @@
<?php // You don't belong here. ?>

View File

@@ -0,0 +1,6 @@
preset: recommended
disabled:
- align_double_arrow
- phpdoc_align
- blank_line_after_opening_tag

View File

@@ -0,0 +1,4 @@
services: docker
script:
- docker-compose run tests

View File

@@ -0,0 +1,6 @@
FROM php:7.3-cli
RUN apt-get update \
&& apt-get install -y unzip \
&& docker-php-ext-install pcntl \
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer

View File

@@ -0,0 +1,13 @@
Copyright 1999-2020. Plesk International GmbH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -0,0 +1,66 @@
## PHP library for Plesk XML-RPC API
[![Build Status](https://travis-ci.com/plesk/api-php-lib.svg?branch=master)](https://travis-ci.com/plesk/api-php-lib) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/plesk/api-php-lib/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/plesk/api-php-lib/?branch=master)
[![StyleCI](https://styleci.io/repos/26514840/shield?branch=master)](https://styleci.io/repos/26514840)
PHP object-oriented library for Plesk XML-RPC API.
## Install Via Composer
[Composer](https://getcomposer.org/) is a preferable way to install the library:
`composer require plesk/api-php-lib`
## Usage Examples
Here is an example on how to use the library and create a customer with desired properties:
```php
$client = new \PleskX\Api\Client($host);
$client->setCredentials($login, $password);
$client->customer()->create([
'cname' => 'Plesk',
'pname' => 'John Smith',
'login' => 'john',
'passwd' => 'secret',
'email' => 'john@smith.com',
]);
```
It is possible to use a secret key instead of password for authentication.
```php
$client = new \PleskX\Api\Client($host);
$client->setSecretKey($secretKey)
```
In case of Plesk extension creation one can use an internal mechanism to access XML-RPC API. It does not require to pass authentication because the extension works in the context of Plesk.
```php
$client = new \PleskX\Api\InternalClient();
$protocols = $client->server()->getProtos();
```
For additional examples see tests/ directory.
## How to Run Unit Tests
One the possible ways to become familiar with the library is to check the unit tests.
To run the unit tests use the following command:
`REMOTE_HOST=your-plesk-host.dom REMOTE_PASSWORD=password composer test`
To use custom port one can provide a URL (e.g. for Docker container):
`REMOTE_URL=https://your-plesk-host.dom:port REMOTE_PASSWORD=password composer test`
One more way to run tests is to use Docker:
`docker-compose run tests`
## Continuous Testing
During active development it could be more convenient to run tests in continuous manner. Here is the way how to achieve it:
`REMOTE_URL=https://your-plesk-host.dom:port REMOTE_PASSWORD=password composer test:watch`

View File

@@ -0,0 +1,48 @@
{
"name": "plesk/api-php-lib",
"type": "library",
"description": "PHP object-oriented library for Plesk XML-RPC API",
"license": "Apache-2.0",
"authors": [
{
"name": "Alexei Yuzhakov",
"email": "sibprogrammer@gmail.com"
},
{
"name": "Plesk International GmbH.",
"email": "plesk-dev-leads@plesk.com"
}
],
"require": {
"php": "^7.3",
"ext-curl": "*",
"ext-xml": "*",
"ext-simplexml": "*"
},
"require-dev": {
"phpunit/phpunit": "^9",
"spatie/phpunit-watcher": "^1.22"
},
"config": {
"process-timeout": 0
},
"scripts": {
"test": "phpunit",
"test:watch": "phpunit-watcher watch"
},
"autoload": {
"psr-4": {
"PleskX\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PleskXTest\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-master": "2.0.x-dev"
}
}
}

View File

@@ -0,0 +1,21 @@
version: '2'
services:
plesk:
image: plesk/plesk
logging:
driver: none
ports:
["8443:8443"]
tests:
build: .
environment:
REMOTE_URL: https://plesk:8443
REMOTE_PASSWORD: changeme1Q**
command: bash -c "cd /opt/api-php-lib && composer install && ./wait-for-plesk.sh && composer test -- --testdox"
depends_on:
- plesk
links:
- plesk
volumes:
- .:/opt/api-php-lib

Some files were not shown because too many files have changed in this diff Show More