save
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
/**
|
||||
* Encode and decode safely in base64.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class Base64SafeEncoder
|
||||
{
|
||||
public function encode(string $input): string
|
||||
{
|
||||
return str_replace('=', '', strtr(base64_encode($input), '+/', '-_'));
|
||||
}
|
||||
|
||||
public function decode(string $input): string
|
||||
{
|
||||
$remainder = \strlen($input) % 4;
|
||||
|
||||
if ($remainder) {
|
||||
$padlen = 4 - $remainder;
|
||||
$input .= str_repeat('=', $padlen);
|
||||
}
|
||||
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreClientException;
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use AcmePhp\Core\Exception\Protocol\ExpectedJsonException;
|
||||
use AcmePhp\Core\Exception\Server\BadNonceServerException;
|
||||
use AcmePhp\Core\Protocol\ExternalAccount;
|
||||
use AcmePhp\Core\Util\JsonDecoder;
|
||||
use AcmePhp\Ssl\KeyPair;
|
||||
use AcmePhp\Ssl\Parser\KeyParser;
|
||||
use AcmePhp\Ssl\Signer\DataSigner;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Header;
|
||||
use GuzzleHttp\Psr7\Request;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Lcobucci\JWT\Signer\Hmac\Sha256;
|
||||
use Lcobucci\JWT\Signer\Key\InMemory;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Guzzle HTTP client wrapper to send requests signed with the account KeyPair.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class SecureHttpClient
|
||||
{
|
||||
/**
|
||||
* @var KeyPair
|
||||
*/
|
||||
private $accountKeyPair;
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* @var Base64SafeEncoder
|
||||
*/
|
||||
private $base64Encoder;
|
||||
|
||||
/**
|
||||
* @var KeyParser
|
||||
*/
|
||||
private $keyParser;
|
||||
|
||||
/**
|
||||
* @var DataSigner
|
||||
*/
|
||||
private $dataSigner;
|
||||
|
||||
/**
|
||||
* @var ServerErrorHandler
|
||||
*/
|
||||
private $errorHandler;
|
||||
|
||||
/**
|
||||
* @var ResponseInterface
|
||||
*/
|
||||
private $lastResponse;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nonceEndpoint;
|
||||
|
||||
public function __construct(
|
||||
KeyPair $accountKeyPair,
|
||||
ClientInterface $httpClient,
|
||||
Base64SafeEncoder $base64Encoder,
|
||||
KeyParser $keyParser,
|
||||
DataSigner $dataSigner,
|
||||
ServerErrorHandler $errorHandler
|
||||
) {
|
||||
$this->accountKeyPair = $accountKeyPair;
|
||||
$this->httpClient = $httpClient;
|
||||
$this->base64Encoder = $base64Encoder;
|
||||
$this->keyParser = $keyParser;
|
||||
$this->dataSigner = $dataSigner;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
public function getJWK(): array
|
||||
{
|
||||
$privateKey = $this->accountKeyPair->getPrivateKey();
|
||||
$parsedKey = $this->keyParser->parse($privateKey);
|
||||
|
||||
switch ($parsedKey->getType()) {
|
||||
case OPENSSL_KEYTYPE_RSA:
|
||||
return [
|
||||
// this order matters
|
||||
'e' => $this->base64Encoder->encode($parsedKey->getDetail('e')),
|
||||
'kty' => 'RSA',
|
||||
'n' => $this->base64Encoder->encode($parsedKey->getDetail('n')),
|
||||
];
|
||||
|
||||
case OPENSSL_KEYTYPE_EC:
|
||||
return [
|
||||
// this order matters
|
||||
'crv' => 'P-'.$parsedKey->getBits(),
|
||||
'kty' => 'EC',
|
||||
'x' => $this->base64Encoder->encode($parsedKey->getDetail('x')),
|
||||
'y' => $this->base64Encoder->encode($parsedKey->getDetail('y')),
|
||||
];
|
||||
|
||||
default:
|
||||
throw new AcmeCoreClientException('Private key type not supported');
|
||||
}
|
||||
}
|
||||
|
||||
public function getJWKThumbprint(): string
|
||||
{
|
||||
return hash('sha256', json_encode($this->getJWK()), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a payload signed with account's KID.
|
||||
*
|
||||
* @param string|array|null $payload
|
||||
*/
|
||||
public function signKidPayload(string $endpoint, string $account, $payload = null, bool $withNonce = true): array
|
||||
{
|
||||
$protected = ['alg' => $this->getAlg(), 'kid' => $account, 'url' => $endpoint];
|
||||
if ($withNonce) {
|
||||
$protected['nonce'] = $this->getNonce();
|
||||
}
|
||||
|
||||
return $this->signPayload($protected, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a payload signed with JWK.
|
||||
*
|
||||
* @param string|array|null $payload
|
||||
*/
|
||||
public function signJwkPayload(string $endpoint, $payload = null, bool $withNonce = true): array
|
||||
{
|
||||
$protected = ['alg' => $this->getAlg(), 'jwk' => $this->getJWK(), 'url' => $endpoint];
|
||||
if ($withNonce) {
|
||||
$protected['nonce'] = $this->getNonce();
|
||||
}
|
||||
|
||||
return $this->signPayload($protected, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an External Account Binding payload signed with JWS.
|
||||
*
|
||||
* @param string|array|null $payload
|
||||
*/
|
||||
public function createExternalAccountPayload(ExternalAccount $externalAccount, string $url): array
|
||||
{
|
||||
$signer = new Sha256();
|
||||
|
||||
$protected = [
|
||||
'alg' => method_exists($signer, 'algorithmId') ? $signer->algorithmId() : $signer->getAlgorithmId(),
|
||||
'kid' => $externalAccount->getId(),
|
||||
'url' => $url,
|
||||
];
|
||||
|
||||
$encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES));
|
||||
$encodedPayload = $this->base64Encoder->encode(json_encode($this->getJWK(), JSON_UNESCAPED_SLASHES));
|
||||
|
||||
$hmacKey = $this->base64Encoder->decode($externalAccount->getHmacKey());
|
||||
$hmacKey = class_exists(InMemory::class) ? InMemory::plainText($hmacKey) : $hmacKey;
|
||||
|
||||
$signature = $this->base64Encoder->encode($signer->sign($encodedProtected.'.'.$encodedPayload, $hmacKey));
|
||||
|
||||
return [
|
||||
'protected' => $encodedProtected,
|
||||
'payload' => $encodedPayload,
|
||||
'signature' => $signature,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request encoded in the format defined by the ACME protocol
|
||||
* and its content (optionally parsed as JSON).
|
||||
*
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
*
|
||||
* @return array|string Array of parsed JSON if $returnJson = true, string otherwise
|
||||
*/
|
||||
public function request(string $method, string $endpoint, array $data = [], bool $returnJson = true)
|
||||
{
|
||||
$response = $this->rawRequest($method, $endpoint, $data);
|
||||
$body = Utils::copyToString($response->getBody());
|
||||
|
||||
if (!$returnJson) {
|
||||
return $body;
|
||||
}
|
||||
|
||||
try {
|
||||
if ('' === $body) {
|
||||
throw new \InvalidArgumentException('Empty body received.');
|
||||
}
|
||||
|
||||
$data = JsonDecoder::decode($body, true);
|
||||
} catch (\InvalidArgumentException $exception) {
|
||||
throw new ExpectedJsonException(sprintf('ACME client expected valid JSON as a response to request "%s %s" (given: "%s")', $method, $endpoint, ServerErrorHandler::getResponseBodySummary($response)), $exception);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a request encoded in the format defined by the ACME protocol and return the response object.
|
||||
*
|
||||
* @throws AcmeCoreClientException when an error occured during response parsing
|
||||
* @throws ExpectedJsonException when $returnJson = true and the response is not valid JSON
|
||||
* @throws AcmeCoreServerException when the ACME server returns an error HTTP status code
|
||||
*/
|
||||
public function rawRequest(string $method, string $endpoint, array $data = []): ResponseInterface
|
||||
{
|
||||
$call = function () use ($method, $endpoint, $data) {
|
||||
$request = $this->createRequest($method, $endpoint, $data);
|
||||
try {
|
||||
$this->lastResponse = $this->httpClient->send($request);
|
||||
} catch (\Exception $exception) {
|
||||
$this->handleClientException($request, $exception);
|
||||
}
|
||||
|
||||
return $request;
|
||||
};
|
||||
|
||||
try {
|
||||
$call();
|
||||
} catch (BadNonceServerException $e) {
|
||||
$call();
|
||||
}
|
||||
|
||||
return $this->lastResponse;
|
||||
}
|
||||
|
||||
public function setAccountKeyPair(KeyPair $keyPair)
|
||||
{
|
||||
$this->accountKeyPair = $keyPair;
|
||||
}
|
||||
|
||||
public function getLastCode(): int
|
||||
{
|
||||
return $this->lastResponse->getStatusCode();
|
||||
}
|
||||
|
||||
public function getLastLocation(): string
|
||||
{
|
||||
return $this->lastResponse->getHeaderLine('Location');
|
||||
}
|
||||
|
||||
public function getLastLinks(): array
|
||||
{
|
||||
return Header::parse($this->lastResponse->getHeader('Link'));
|
||||
}
|
||||
|
||||
public function getAccountKeyPair(): KeyPair
|
||||
{
|
||||
return $this->accountKeyPair;
|
||||
}
|
||||
|
||||
public function getKeyParser(): KeyParser
|
||||
{
|
||||
return $this->keyParser;
|
||||
}
|
||||
|
||||
public function getDataSigner(): DataSigner
|
||||
{
|
||||
return $this->dataSigner;
|
||||
}
|
||||
|
||||
public function setNonceEndpoint(string $endpoint)
|
||||
{
|
||||
$this->nonceEndpoint = $endpoint;
|
||||
}
|
||||
|
||||
public function getBase64Encoder(): Base64SafeEncoder
|
||||
{
|
||||
return $this->base64Encoder;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the given Payload.
|
||||
*/
|
||||
private function signPayload(array $protected, array $payload = null): array
|
||||
{
|
||||
if (!isset($protected['alg'])) {
|
||||
throw new \InvalidArgumentException('The property "alg" is required in the protected array');
|
||||
}
|
||||
|
||||
$alg = $protected['alg'];
|
||||
|
||||
$privateKey = $this->accountKeyPair->getPrivateKey();
|
||||
list($algorithm, $format) = $this->extractSignOptionFromJWSAlg($alg);
|
||||
|
||||
$encodedProtected = $this->base64Encoder->encode(json_encode($protected, JSON_UNESCAPED_SLASHES));
|
||||
|
||||
if (null === $payload) {
|
||||
$encodedPayload = '';
|
||||
} elseif ([] === $payload) {
|
||||
$encodedPayload = $this->base64Encoder->encode('{}');
|
||||
} else {
|
||||
$encodedPayload = $this->base64Encoder->encode(json_encode($payload, JSON_UNESCAPED_SLASHES));
|
||||
}
|
||||
|
||||
$signature = $this->base64Encoder->encode(
|
||||
$this->dataSigner->signData($encodedProtected.'.'.$encodedPayload, $privateKey, $algorithm, $format)
|
||||
);
|
||||
|
||||
return [
|
||||
'protected' => $encodedProtected,
|
||||
'payload' => $encodedPayload,
|
||||
'signature' => $signature,
|
||||
];
|
||||
}
|
||||
|
||||
private function createRequest($method, $endpoint, $data)
|
||||
{
|
||||
$request = new Request($method, $endpoint);
|
||||
$request = $request->withHeader('Accept', 'application/json,application/jose+json,');
|
||||
|
||||
if ('POST' === $method && \is_array($data)) {
|
||||
$request = $request->withHeader('Content-Type', 'application/jose+json');
|
||||
$request = $request->withBody(Utils::streamFor(json_encode($data)));
|
||||
}
|
||||
|
||||
return $request;
|
||||
}
|
||||
|
||||
private function handleClientException(Request $request, \Exception $exception)
|
||||
{
|
||||
if ($exception instanceof RequestException && $exception->getResponse() instanceof ResponseInterface) {
|
||||
$this->lastResponse = $exception->getResponse();
|
||||
|
||||
throw $this->errorHandler->createAcmeExceptionForResponse($request, $this->lastResponse, $exception);
|
||||
}
|
||||
|
||||
throw new AcmeCoreClientException(sprintf('An error occured during request "%s %s"', $request->getMethod(), $request->getUri()), $exception);
|
||||
}
|
||||
|
||||
private function getNonce(): ?string
|
||||
{
|
||||
if ($this->lastResponse && $this->lastResponse->hasHeader('Replay-Nonce')) {
|
||||
return $this->lastResponse->getHeaderLine('Replay-Nonce');
|
||||
}
|
||||
|
||||
if (null !== $this->nonceEndpoint) {
|
||||
$this->request('HEAD', $this->nonceEndpoint, [], false);
|
||||
|
||||
if ($this->lastResponse->hasHeader('Replay-Nonce')) {
|
||||
return $this->lastResponse->getHeaderLine('Replay-Nonce');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function getAlg(): string
|
||||
{
|
||||
$privateKey = $this->accountKeyPair->getPrivateKey();
|
||||
$parsedKey = $this->keyParser->parse($privateKey);
|
||||
|
||||
switch ($parsedKey->getType()) {
|
||||
case OPENSSL_KEYTYPE_RSA:
|
||||
return 'RS256';
|
||||
|
||||
case OPENSSL_KEYTYPE_EC:
|
||||
switch ($parsedKey->getBits()) {
|
||||
case 256:
|
||||
case 384:
|
||||
return 'ES'.$parsedKey->getBits();
|
||||
case 521:
|
||||
return 'ES512';
|
||||
}
|
||||
|
||||
// no break to let the default case
|
||||
default:
|
||||
throw new AcmeCoreClientException('Private key type is not supported');
|
||||
}
|
||||
}
|
||||
|
||||
private function extractSignOptionFromJWSAlg($alg): array
|
||||
{
|
||||
if (!preg_match('/^([A-Z]+)(\d+)$/', $alg, $match)) {
|
||||
throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg));
|
||||
}
|
||||
|
||||
if (!\defined('OPENSSL_ALGO_SHA'.$match[2])) {
|
||||
throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg));
|
||||
}
|
||||
|
||||
$algorithm = \constant('OPENSSL_ALGO_SHA'.$match[2]);
|
||||
|
||||
switch ($match[1]) {
|
||||
case 'RS':
|
||||
$format = DataSigner::FORMAT_DER;
|
||||
break;
|
||||
|
||||
case 'ES':
|
||||
$format = DataSigner::FORMAT_ECDSA;
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new AcmeCoreClientException(sprintf('The given "%s" algorithm is not supported', $alg));
|
||||
}
|
||||
|
||||
return [$algorithm, $format];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
use AcmePhp\Ssl\KeyPair;
|
||||
use AcmePhp\Ssl\Parser\KeyParser;
|
||||
use AcmePhp\Ssl\Signer\DataSigner;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
|
||||
/**
|
||||
* Guzzle HTTP client wrapper to send requests signed with the account KeyPair.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class SecureHttpClientFactory
|
||||
{
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* @var Base64SafeEncoder
|
||||
*/
|
||||
private $base64Encoder;
|
||||
|
||||
/**
|
||||
* @var KeyParser
|
||||
*/
|
||||
private $keyParser;
|
||||
|
||||
/**
|
||||
* @var DataSigner
|
||||
*/
|
||||
private $dataSigner;
|
||||
|
||||
/**
|
||||
* @var ServerErrorHandler
|
||||
*/
|
||||
private $errorHandler;
|
||||
|
||||
public function __construct(
|
||||
ClientInterface $httpClient,
|
||||
Base64SafeEncoder $base64Encoder,
|
||||
KeyParser $keyParser,
|
||||
DataSigner $dataSigner,
|
||||
ServerErrorHandler $errorHandler
|
||||
) {
|
||||
$this->httpClient = $httpClient;
|
||||
$this->base64Encoder = $base64Encoder;
|
||||
$this->keyParser = $keyParser;
|
||||
$this->dataSigner = $dataSigner;
|
||||
$this->errorHandler = $errorHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a SecureHttpClient using a given account KeyPair.
|
||||
*/
|
||||
public function createSecureHttpClient(KeyPair $accountKeyPair): SecureHttpClient
|
||||
{
|
||||
return new SecureHttpClient(
|
||||
$accountKeyPair,
|
||||
$this->httpClient,
|
||||
$this->base64Encoder,
|
||||
$this->keyParser,
|
||||
$this->dataSigner,
|
||||
$this->errorHandler
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Acme PHP project.
|
||||
*
|
||||
* (c) Titouan Galopin <galopintitouan@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace AcmePhp\Core\Http;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeCoreServerException;
|
||||
use AcmePhp\Core\Exception\Server\BadCsrServerException;
|
||||
use AcmePhp\Core\Exception\Server\BadNonceServerException;
|
||||
use AcmePhp\Core\Exception\Server\CaaServerException;
|
||||
use AcmePhp\Core\Exception\Server\ConnectionServerException;
|
||||
use AcmePhp\Core\Exception\Server\DnsServerException;
|
||||
use AcmePhp\Core\Exception\Server\IncorrectResponseServerException;
|
||||
use AcmePhp\Core\Exception\Server\InternalServerException;
|
||||
use AcmePhp\Core\Exception\Server\InvalidContactServerException;
|
||||
use AcmePhp\Core\Exception\Server\InvalidEmailServerException;
|
||||
use AcmePhp\Core\Exception\Server\MalformedServerException;
|
||||
use AcmePhp\Core\Exception\Server\OrderNotReadyServerException;
|
||||
use AcmePhp\Core\Exception\Server\RateLimitedServerException;
|
||||
use AcmePhp\Core\Exception\Server\RejectedIdentifierServerException;
|
||||
use AcmePhp\Core\Exception\Server\TlsServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnauthorizedServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnknownHostServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnsupportedContactServerException;
|
||||
use AcmePhp\Core\Exception\Server\UnsupportedIdentifierServerException;
|
||||
use AcmePhp\Core\Exception\Server\UserActionRequiredServerException;
|
||||
use AcmePhp\Core\Util\JsonDecoder;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use GuzzleHttp\Psr7\Utils;
|
||||
use Psr\Http\Message\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
/**
|
||||
* Create appropriate exception for given server response.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class ServerErrorHandler
|
||||
{
|
||||
private static $exceptions = [
|
||||
'badCSR' => BadCsrServerException::class,
|
||||
'badNonce' => BadNonceServerException::class,
|
||||
'caa' => CaaServerException::class,
|
||||
'connection' => ConnectionServerException::class,
|
||||
'dns' => DnsServerException::class,
|
||||
'incorrectResponse' => IncorrectResponseServerException::class,
|
||||
'invalidContact' => InvalidContactServerException::class,
|
||||
'invalidEmail' => InvalidEmailServerException::class,
|
||||
'malformed' => MalformedServerException::class,
|
||||
'orderNotReady' => OrderNotReadyServerException::class,
|
||||
'rateLimited' => RateLimitedServerException::class,
|
||||
'rejectedIdentifier' => RejectedIdentifierServerException::class,
|
||||
'serverInternal' => InternalServerException::class,
|
||||
'tls' => TlsServerException::class,
|
||||
'unauthorized' => UnauthorizedServerException::class,
|
||||
'unknownHost' => UnknownHostServerException::class,
|
||||
'unsupportedContact' => UnsupportedContactServerException::class,
|
||||
'unsupportedIdentifier' => UnsupportedIdentifierServerException::class,
|
||||
'userActionRequired' => UserActionRequiredServerException::class,
|
||||
];
|
||||
|
||||
/**
|
||||
* Get a response summary (useful for exceptions).
|
||||
* Use Guzzle method if available (Guzzle 6.1.1+).
|
||||
*/
|
||||
public static function getResponseBodySummary(ResponseInterface $response): string
|
||||
{
|
||||
// Rewind the stream if possible to allow re-reading for the summary.
|
||||
if ($response->getBody()->isSeekable()) {
|
||||
$response->getBody()->rewind();
|
||||
}
|
||||
|
||||
if (method_exists(RequestException::class, 'getResponseBodySummary')) {
|
||||
return RequestException::getResponseBodySummary($response);
|
||||
}
|
||||
|
||||
$body = Utils::copyToString($response->getBody());
|
||||
|
||||
if (\strlen($body) > 120) {
|
||||
return substr($body, 0, 120).' (truncated...)';
|
||||
}
|
||||
|
||||
return $body;
|
||||
}
|
||||
|
||||
public function createAcmeExceptionForResponse(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
\Exception $previous = null
|
||||
): AcmeCoreServerException {
|
||||
$body = Utils::copyToString($response->getBody());
|
||||
|
||||
try {
|
||||
$data = JsonDecoder::decode($body, true);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$data = null;
|
||||
}
|
||||
|
||||
if (!$data || !isset($data['type'], $data['detail'])) {
|
||||
// Not JSON: not an ACME error response
|
||||
return $this->createDefaultExceptionForResponse($request, $response, $previous);
|
||||
}
|
||||
|
||||
$type = preg_replace('/^urn:(ietf:params:)?acme:error:/i', '', $data['type']);
|
||||
|
||||
if (!isset(self::$exceptions[$type])) {
|
||||
// Unknown type: not an ACME error response
|
||||
return $this->createDefaultExceptionForResponse($request, $response, $previous);
|
||||
}
|
||||
|
||||
$exceptionClass = self::$exceptions[$type];
|
||||
|
||||
return new $exceptionClass(
|
||||
$request,
|
||||
sprintf('%s (on request "%s %s")', $data['detail'], $request->getMethod(), $request->getUri()),
|
||||
$previous
|
||||
);
|
||||
}
|
||||
|
||||
private function createDefaultExceptionForResponse(
|
||||
RequestInterface $request,
|
||||
ResponseInterface $response,
|
||||
\Exception $previous = null
|
||||
): AcmeCoreServerException {
|
||||
return new AcmeCoreServerException(
|
||||
$request,
|
||||
sprintf(
|
||||
'A non-ACME %s HTTP error occured on request "%s %s" (response body: "%s")',
|
||||
$response->getStatusCode(),
|
||||
$request->getMethod(),
|
||||
$request->getUri(),
|
||||
self::getResponseBodySummary($response)
|
||||
),
|
||||
$previous
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user