save
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
<?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\Challenge;
|
||||
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeNotSupportedException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* A strategy ACME validator.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class ChainValidator implements ValidatorInterface
|
||||
{
|
||||
/** @var ValidatorInterface[] */
|
||||
private $validators;
|
||||
|
||||
/**
|
||||
* @param ValidatorInterface[] $validators
|
||||
*/
|
||||
public function __construct(array $validators)
|
||||
{
|
||||
$this->validators = $validators;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
foreach ($this->validators as $validator) {
|
||||
if ($validator->supports($authorizationChallenge, $solver)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
foreach ($this->validators as $validator) {
|
||||
if ($validator->supports($authorizationChallenge, $solver)) {
|
||||
return $validator->isValid($authorizationChallenge, $solver);
|
||||
}
|
||||
}
|
||||
|
||||
throw new ChallengeNotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?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\Challenge;
|
||||
|
||||
/**
|
||||
* ACME challenge solver.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface ConfigurableServiceInterface
|
||||
{
|
||||
/**
|
||||
* Configure the service with a set of configuration.
|
||||
*/
|
||||
public function configure(array $config);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Http\Base64SafeEncoder;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Extract data needed to solve DNS challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DnsDataExtractor
|
||||
{
|
||||
/** @var Base64SafeEncoder */
|
||||
private $encoder;
|
||||
|
||||
public function __construct(Base64SafeEncoder $encoder = null)
|
||||
{
|
||||
$this->encoder = $encoder ?: new Base64SafeEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the name of the TXT record to register.
|
||||
*/
|
||||
public function getRecordName(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return sprintf('_acme-challenge.%s.', $authorizationChallenge->getDomain());
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the value of the TXT record to register.
|
||||
*/
|
||||
public function getRecordValue(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return $this->encoder->encode(hash('sha256', $authorizationChallenge->getPayload(), true));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
/**
|
||||
* Resolves DNS.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface DnsResolverInterface
|
||||
{
|
||||
/**
|
||||
* Return whether or not the Resolver is supported.
|
||||
*/
|
||||
public static function isSupported(): bool;
|
||||
|
||||
/**
|
||||
* Retrieves the list of TXT entries for the given domain.
|
||||
*/
|
||||
public function getTxtEntries(string $domain): array;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Challenge\ValidatorInterface;
|
||||
use AcmePhp\Core\Exception\AcmeDnsResolutionException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Validator for DNS challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class DnsValidator implements ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var DnsResolverInterface
|
||||
*/
|
||||
private $dnsResolver;
|
||||
|
||||
public function __construct(DnsDataExtractor $extractor = null, DnsResolverInterface $dnsResolver = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
|
||||
$this->dnsResolver = $dnsResolver;
|
||||
if (!$this->dnsResolver) {
|
||||
$this->dnsResolver = LibDnsResolver::isSupported() ? new LibDnsResolver() : new SimpleDnsResolver();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
|
||||
|
||||
try {
|
||||
return \in_array($recordValue, $this->dnsResolver->getTxtEntries($recordName), false);
|
||||
} catch (AcmeDnsResolutionException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\ConfigurableServiceInterface;
|
||||
use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME DNS solver with automate configuration of a Gandi.Net.
|
||||
*
|
||||
* @author Alexander Obuhovich <aik.bold@gmail.com>
|
||||
*/
|
||||
class GandiSolver implements MultipleChallengesSolverInterface, ConfigurableServiceInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var ClientInterface
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $cacheZones;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $apiKey;
|
||||
|
||||
public function __construct(DnsDataExtractor $extractor = null, ClientInterface $client = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
$this->client = $client ?: new Client();
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the service with a set of configuration.
|
||||
*/
|
||||
public function configure(array $config)
|
||||
{
|
||||
$this->apiKey = $config['api_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->solveAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solveAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain());
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
|
||||
|
||||
$subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName);
|
||||
|
||||
$this->client->request(
|
||||
'PUT',
|
||||
'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT',
|
||||
[
|
||||
'headers' => [
|
||||
'X-Api-Key' => $this->apiKey,
|
||||
],
|
||||
'json' => [
|
||||
'rrset_type' => 'TXT',
|
||||
'rrset_ttl' => 600,
|
||||
'rrset_name' => $subDomain,
|
||||
'rrset_values' => [$recordValue],
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->cleanupAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanupAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$topLevelDomain = $this->getTopLevelDomain($authorizationChallenge->getDomain());
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
|
||||
$subDomain = \str_replace('.'.$topLevelDomain.'.', '', $recordName);
|
||||
|
||||
$this->client->request(
|
||||
'DELETE',
|
||||
'https://dns.api.gandi.net/api/v5/domains/'.$topLevelDomain.'/records/'.$subDomain.'/TXT',
|
||||
[
|
||||
'headers' => [
|
||||
'X-Api-Key' => $this->apiKey,
|
||||
],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected function getTopLevelDomain(string $domain): string
|
||||
{
|
||||
return \implode('.', \array_slice(\explode('.', $domain), -2));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Exception\AcmeDnsResolutionException;
|
||||
use LibDNS\Decoder\Decoder;
|
||||
use LibDNS\Decoder\DecoderFactory;
|
||||
use LibDNS\Encoder\Encoder;
|
||||
use LibDNS\Encoder\EncoderFactory;
|
||||
use LibDNS\Messages\MessageFactory;
|
||||
use LibDNS\Messages\MessageTypes;
|
||||
use LibDNS\Records\QuestionFactory;
|
||||
use LibDNS\Records\ResourceTypes;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
|
||||
/**
|
||||
* Resolves DNS with LibDNS (pass over internal DNS cache and check several nameservers).
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class LibDnsResolver implements DnsResolverInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var QuestionFactory
|
||||
*/
|
||||
private $questionFactory;
|
||||
|
||||
/**
|
||||
* @var MessageFactory
|
||||
*/
|
||||
private $messageFactory;
|
||||
|
||||
/**
|
||||
* @var Encoder
|
||||
*/
|
||||
private $encoder;
|
||||
|
||||
/**
|
||||
* @var Decoder
|
||||
*/
|
||||
private $decoder;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $nameServer;
|
||||
|
||||
public function __construct(
|
||||
QuestionFactory $questionFactory = null,
|
||||
MessageFactory $messageFactory = null,
|
||||
Encoder $encoder = null,
|
||||
Decoder $decoder = null,
|
||||
$nameServer = '8.8.8.8'
|
||||
) {
|
||||
$this->questionFactory = $questionFactory ?: new QuestionFactory();
|
||||
$this->messageFactory = $messageFactory ?: new MessageFactory();
|
||||
$this->encoder = $encoder ?: (new EncoderFactory())->create();
|
||||
$this->decoder = $decoder ?: (new DecoderFactory())->create();
|
||||
$this->nameServer = $nameServer;
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
return class_exists(ResourceTypes::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public function getTxtEntries($domain): array
|
||||
{
|
||||
$domain = rtrim($domain, '.');
|
||||
$nameServers = $this->getNameServers($domain);
|
||||
$this->logger->debug('Fetched TXT records for domain', ['nsDomain' => $domain, 'servers' => $nameServers]);
|
||||
$identicalEntries = [];
|
||||
foreach ($nameServers as $nameServer) {
|
||||
$ipNameServer = gethostbynamel($nameServer);
|
||||
if (empty($ipNameServer)) {
|
||||
throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer));
|
||||
}
|
||||
try {
|
||||
$response = $this->request($domain, ResourceTypes::TXT, $ipNameServer[0]);
|
||||
} catch (\Exception $e) {
|
||||
throw new AcmeDnsResolutionException(sprintf('Unable to find domain %s on nameserver %s', $domain, $nameServer));
|
||||
}
|
||||
$entries = [];
|
||||
foreach ($response->getAnswerRecords() as $record) {
|
||||
foreach ($record->getData() as $recordData) {
|
||||
$entries[] = (string) $recordData;
|
||||
}
|
||||
}
|
||||
|
||||
$identicalEntries[json_encode($entries)][] = $nameServer;
|
||||
}
|
||||
|
||||
$this->logger->info('DNS records fetched', ['mapping' => $identicalEntries]);
|
||||
if (1 !== \count($identicalEntries)) {
|
||||
throw new AcmeDnsResolutionException('Dns not fully propagated');
|
||||
}
|
||||
|
||||
return json_decode(key($identicalEntries));
|
||||
}
|
||||
|
||||
private function getNameServers($domain)
|
||||
{
|
||||
if ('' === $domain) {
|
||||
return [$this->nameServer];
|
||||
}
|
||||
|
||||
$parentNameServers = $this->getNameServers(implode('.', \array_slice(explode('.', $domain), 1)));
|
||||
$itemNameServers = [];
|
||||
$this->logger->debug('Fetched NS in charge of domain', ['nsDomain' => $domain, 'servers' => $parentNameServers]);
|
||||
foreach ($parentNameServers as $parentNameServer) {
|
||||
$ipNameServer = gethostbynamel($parentNameServer);
|
||||
if (empty($ipNameServer)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
$response = $this->request(
|
||||
$domain,
|
||||
ResourceTypes::NS,
|
||||
$ipNameServer[0]
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// ignore errors
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($response->getAnswerRecords() as $record) {
|
||||
try {
|
||||
$field = $record->getData()->getFieldByName('nsdname');
|
||||
$itemNameServers[] = $field->getValue();
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
}
|
||||
}
|
||||
foreach ($response->getAuthorityRecords() as $record) {
|
||||
try {
|
||||
$field = $record->getData()->getFieldByName('nsdname');
|
||||
$itemNameServers[] = $field->getValue();
|
||||
} catch (\OutOfBoundsException $e) {
|
||||
}
|
||||
}
|
||||
}
|
||||
$itemNameServers = array_unique($itemNameServers);
|
||||
if (empty($itemNameServers)) {
|
||||
return $parentNameServers;
|
||||
}
|
||||
|
||||
return $itemNameServers;
|
||||
}
|
||||
|
||||
private function request($domain, $type, $nameServer)
|
||||
{
|
||||
$question = $this->questionFactory->create($type);
|
||||
$question->setName($domain);
|
||||
|
||||
// Create request message
|
||||
$request = $this->messageFactory->create(MessageTypes::QUERY);
|
||||
$request->getQuestionRecords()->add($question);
|
||||
$request->isRecursionDesired(true);
|
||||
|
||||
// Send request
|
||||
$socket = stream_socket_client(sprintf('udp://'.$nameServer.':53'));
|
||||
stream_socket_sendto($socket, $this->encoder->encode($request));
|
||||
|
||||
$r = [$socket];
|
||||
$w = $e = [];
|
||||
if (!stream_select($r, $w, $e, 3)) {
|
||||
throw new AcmeDnsResolutionException(sprintf('Timeout reached when requesting ServerName %s', $nameServer));
|
||||
}
|
||||
|
||||
// Decode response message
|
||||
try {
|
||||
$response = $this->decoder->decode(fread($socket, 1 << 20));
|
||||
} catch (\Exception $e) {
|
||||
throw new AcmeDnsResolutionException('Failed to decode server response', $e);
|
||||
}
|
||||
if (0 !== $response->getResponseCode()) {
|
||||
throw new AcmeDnsResolutionException(sprintf('ServerName respond with error code "%d"', $response->getResponseCode()));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,295 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\MultipleChallengesSolverInterface;
|
||||
use AcmePhp\Core\Exception\Protocol\ChallengeFailedException;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Aws\Route53\Route53Client;
|
||||
use Psr\Log\LoggerAwareTrait;
|
||||
use Psr\Log\NullLogger;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME DNS solver with automate configuration of a AWS route53.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class Route53Solver implements MultipleChallengesSolverInterface
|
||||
{
|
||||
use LoggerAwareTrait;
|
||||
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var Route53Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
private $cacheZones;
|
||||
|
||||
public function __construct(DnsDataExtractor $extractor = null, Route53Client $client = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
$this->client = $client ?: new Route53Client([]);
|
||||
$this->logger = new NullLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->solveAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solveAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
$changesPerZone = [];
|
||||
$authorizationChallengesPerDomain = $this->groupAuthorizationChallengesPerDomain($authorizationChallenges);
|
||||
foreach ($authorizationChallengesPerDomain as $domain => $authorizationChallengesForDomain) {
|
||||
$zone = $this->getZone($authorizationChallengesForDomain[0]->getDomain());
|
||||
|
||||
$authorizationChallengesPerRecordName = $this->groupAuthorizationChallengesPerRecordName($authorizationChallengesForDomain);
|
||||
foreach ($authorizationChallengesPerRecordName as $recordName => $authorizationChallengesForRecordName) {
|
||||
$challengeValues = array_unique(array_map([$this->extractor, 'getRecordValue'], $authorizationChallengesForRecordName));
|
||||
$recordIndex = $this->getPreviousRecordIndex($zone['Id'], $recordName);
|
||||
|
||||
if (0 === \count(array_diff($challengeValues, array_keys($recordIndex)))) {
|
||||
$this->logger->debug('Record already defined', ['recordName' => $recordName]);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($challengeValues as $recordValue) {
|
||||
$recordIndex[$recordValue] = time();
|
||||
}
|
||||
|
||||
$changesPerZone[$zone['Id']][] = $this->getSaveRecordQuery($recordName, $recordIndex);
|
||||
}
|
||||
}
|
||||
|
||||
$records = [];
|
||||
foreach ($changesPerZone as $zoneId => $changes) {
|
||||
$this->logger->info('Updating route 53 DNS', ['zone' => $zoneId]);
|
||||
$records[$zoneId] = $this->client->changeResourceRecordSets(
|
||||
[
|
||||
'ChangeBatch' => [
|
||||
'Changes' => $changes,
|
||||
],
|
||||
'HostedZoneId' => $zoneId,
|
||||
]
|
||||
);
|
||||
}
|
||||
foreach ($records as $zoneId => $record) {
|
||||
$this->logger->info('Waiting for Route 53 changes', ['zone' => $zoneId]);
|
||||
$this->client->waitUntil('ResourceRecordSetsChanged', ['Id' => $record['ChangeInfo']['Id']]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
return $this->cleanupAll([$authorizationChallenge]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanupAll(array $authorizationChallenges)
|
||||
{
|
||||
Assert::allIsInstanceOf($authorizationChallenges, AuthorizationChallenge::class);
|
||||
|
||||
$changesPerZone = [];
|
||||
$authorizationChallengesPerDomain = $this->groupAuthorizationChallengesPerDomain($authorizationChallenges);
|
||||
foreach ($authorizationChallengesPerDomain as $domain => $authorizationChallengesForDomain) {
|
||||
$zone = $this->getZone($authorizationChallengesForDomain[0]->getDomain());
|
||||
|
||||
$authorizationChallengesPerRecordName = $this->groupAuthorizationChallengesPerRecordName($authorizationChallengesForDomain);
|
||||
foreach ($authorizationChallengesPerRecordName as $recordName => $authorizationChallengesForRecordName) {
|
||||
$challengeValues = array_unique(array_map([$this->extractor, 'getRecordValue'], $authorizationChallengesForRecordName));
|
||||
$recordIndex = $this->getPreviousRecordIndex($zone['Id'], $recordName);
|
||||
|
||||
foreach ($challengeValues as $recordValue) {
|
||||
unset($recordIndex[$recordValue]);
|
||||
}
|
||||
$changesPerZone[$zone['Id']][] = $this->getSaveRecordQuery($recordName, $recordIndex);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($changesPerZone as $zoneId => $changes) {
|
||||
$this->logger->info('Updating route 53 DNS', ['zone' => $zoneId]);
|
||||
$this->client->changeResourceRecordSets(
|
||||
[
|
||||
'ChangeBatch' => [
|
||||
'Changes' => $changes,
|
||||
],
|
||||
'HostedZoneId' => $zoneId,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function getPreviousRecordIndex($zoneId, $recordName)
|
||||
{
|
||||
$previousRecordSets = $this->client->listResourceRecordSets([
|
||||
'HostedZoneId' => $zoneId,
|
||||
'StartRecordName' => $recordName,
|
||||
'StartRecordType' => 'TXT',
|
||||
]);
|
||||
$recordSets = array_filter(
|
||||
$previousRecordSets['ResourceRecordSets'],
|
||||
function ($recordSet) use ($recordName) {
|
||||
return $recordSet['Name'] === $recordName && 'TXT' === $recordSet['Type'];
|
||||
}
|
||||
);
|
||||
$recordIndex = [];
|
||||
foreach ($recordSets as $previousRecordSet) {
|
||||
$previousTxt = array_map(function ($resourceRecord) {
|
||||
return stripslashes(trim($resourceRecord['Value'], '"'));
|
||||
}, $previousRecordSet['ResourceRecords']);
|
||||
// Search the special Index
|
||||
foreach ($previousTxt as $index => $recordValue) {
|
||||
if (null !== $previousIndex = json_decode($recordValue, true)) {
|
||||
$recordIndex = $previousIndex;
|
||||
unset($previousTxt[$index]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Set default value
|
||||
foreach ($previousTxt as $recordValue) {
|
||||
if (!isset($recordIndex[$recordValue])) {
|
||||
$recordIndex[$recordValue] = time();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $recordIndex;
|
||||
}
|
||||
|
||||
private function getSaveRecordQuery($recordName, array $recordIndex)
|
||||
{
|
||||
//remove old indexes
|
||||
$limitTime = time() - 86400;
|
||||
foreach ($recordIndex as $recordValue => $time) {
|
||||
if ($time < $limitTime) {
|
||||
unset($recordIndex[$recordValue]);
|
||||
}
|
||||
}
|
||||
|
||||
$recordValues = array_keys($recordIndex);
|
||||
$recordValues[] = json_encode($recordIndex);
|
||||
|
||||
return [
|
||||
'Action' => 'UPSERT',
|
||||
'ResourceRecordSet' => [
|
||||
'Name' => $recordName,
|
||||
'ResourceRecords' => array_map(function ($recordValue) {
|
||||
return [
|
||||
'Value' => sprintf('"%s"', addslashes($recordValue)),
|
||||
];
|
||||
}, $recordValues),
|
||||
'TTL' => 5,
|
||||
'Type' => 'TXT',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*
|
||||
* @return AuthorizationChallenge[][]
|
||||
*/
|
||||
private function groupAuthorizationChallengesPerDomain(array $authorizationChallenges)
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$groups[$authorizationChallenge->getDomain()][] = $authorizationChallenge;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*
|
||||
* @return AuthorizationChallenge[][]
|
||||
*/
|
||||
private function groupAuthorizationChallengesPerRecordName(array $authorizationChallenges)
|
||||
{
|
||||
$groups = [];
|
||||
foreach ($authorizationChallenges as $authorizationChallenge) {
|
||||
$groups[$this->extractor->getRecordName($authorizationChallenge)][] = $authorizationChallenge;
|
||||
}
|
||||
|
||||
return $groups;
|
||||
}
|
||||
|
||||
private function getZone($domain)
|
||||
{
|
||||
$domainParts = explode('.', $domain);
|
||||
$domains = array_reverse(array_map(
|
||||
function ($index) use ($domainParts) {
|
||||
return implode('.', \array_slice($domainParts, \count($domainParts) - $index));
|
||||
},
|
||||
range(0, \count($domainParts))
|
||||
));
|
||||
|
||||
$zones = $this->getZones();
|
||||
foreach ($domains as $cursorDomain) {
|
||||
if (isset($zones[$cursorDomain.'.'])) {
|
||||
return $zones[$cursorDomain.'.'];
|
||||
}
|
||||
}
|
||||
|
||||
throw new ChallengeFailedException(sprintf('Unable to find a zone for the domain "%s"', $domain));
|
||||
}
|
||||
|
||||
private function getZones()
|
||||
{
|
||||
if (null !== $this->cacheZones) {
|
||||
return $this->cacheZones;
|
||||
}
|
||||
|
||||
$zones = [];
|
||||
$args = [];
|
||||
do {
|
||||
$resp = $this->client->listHostedZones($args);
|
||||
$zones = array_merge($zones, $resp['HostedZones']);
|
||||
$args = ['Marker' => $resp['NextMarker']];
|
||||
} while ($resp['IsTruncated']);
|
||||
|
||||
$this->cacheZones = array_column($zones, null, 'Name');
|
||||
|
||||
return $this->cacheZones;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
/**
|
||||
* Resolves DNS through dns_get_record.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SimpleDnsResolver implements DnsResolverInterface
|
||||
{
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public static function isSupported(): bool
|
||||
{
|
||||
return \function_exists('dns_get_record');
|
||||
}
|
||||
|
||||
/**
|
||||
* @{@inheritdoc}
|
||||
*/
|
||||
public function getTxtEntries($domain): array
|
||||
{
|
||||
$entries = [];
|
||||
foreach (dns_get_record($domain, DNS_TXT) as $record) {
|
||||
$entries = array_merge($entries, $record['entries']);
|
||||
}
|
||||
|
||||
sort($entries);
|
||||
|
||||
return array_unique($entries);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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\Challenge\Dns;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* ACME DNS solver with manual intervention.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SimpleDnsSolver implements SolverInterface
|
||||
{
|
||||
/**
|
||||
* @var DnsDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
protected $output;
|
||||
|
||||
/**
|
||||
* @param DnsDataExtractor $extractor
|
||||
* @param OutputInterface $output
|
||||
*/
|
||||
public function __construct(DnsDataExtractor $extractor = null, OutputInterface $output = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new DnsDataExtractor();
|
||||
$this->output = $output ?: new NullOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'dns-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
$recordValue = $this->extractor->getRecordValue($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
Add the following TXT record to your DNS zone
|
||||
Domain: %s
|
||||
TXT value: %s
|
||||
|
||||
<comment>Wait for the propagation before moving to the next step</comment>
|
||||
Tips: Use the following command to check the propagation
|
||||
|
||||
host -t TXT %s
|
||||
|
||||
EOF
|
||||
,
|
||||
$recordName,
|
||||
$recordValue,
|
||||
$recordName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$recordName = $this->extractor->getRecordName($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
You can now cleanup your DNS by removing the domain <comment>_acme-challenge.%s.</comment>
|
||||
EOF
|
||||
,
|
||||
$recordName
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?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\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\ConfigurableServiceInterface;
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Filesystem\Adapter\NullAdapter;
|
||||
use AcmePhp\Core\Filesystem\FilesystemFactoryInterface;
|
||||
use AcmePhp\Core\Filesystem\FilesystemInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* ACME HTTP solver through ftp upload.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class FilesystemSolver implements SolverInterface, ConfigurableServiceInterface
|
||||
{
|
||||
/**
|
||||
* @var ContainerInterface
|
||||
*/
|
||||
private $filesystemFactoryLocator;
|
||||
|
||||
/**
|
||||
* @var FilesystemInterface
|
||||
*/
|
||||
private $filesystem;
|
||||
|
||||
/**
|
||||
* @var HttpDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
public function __construct(ContainerInterface $filesystemFactoryLocator = null, HttpDataExtractor $extractor = null)
|
||||
{
|
||||
$this->filesystemFactoryLocator = $filesystemFactoryLocator ?: new ServiceLocator([]);
|
||||
$this->extractor = $extractor ?: new HttpDataExtractor();
|
||||
$this->filesystem = new NullAdapter();
|
||||
}
|
||||
|
||||
public function configure(array $config)
|
||||
{
|
||||
Assert::keyExists($config, 'adapter', 'configure::$config expected an array with the key %s.');
|
||||
|
||||
/** @var FilesystemFactoryInterface $factory */
|
||||
$factory = $this->filesystemFactoryLocator->get($config['adapter']);
|
||||
$this->filesystem = $factory->create($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkPath = $this->extractor->getCheckPath($authorizationChallenge);
|
||||
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
|
||||
|
||||
$this->filesystem->write($checkPath, $checkContent);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkPath = $this->extractor->getCheckPath($authorizationChallenge);
|
||||
|
||||
$this->filesystem->delete($checkPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?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\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Extract data needed to solve HTTP challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class HttpDataExtractor
|
||||
{
|
||||
/**
|
||||
* Retrieves the absolute URL called by the CA.
|
||||
*/
|
||||
public function getCheckUrl(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return sprintf(
|
||||
'http://%s%s',
|
||||
$authorizationChallenge->getDomain(),
|
||||
$this->getCheckPath($authorizationChallenge)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the absolute path called by the CA.
|
||||
*/
|
||||
public function getCheckPath(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return sprintf(
|
||||
'/.well-known/acme-challenge/%s',
|
||||
$authorizationChallenge->getToken()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the content that should be returned in the response.
|
||||
*/
|
||||
public function getCheckContent(AuthorizationChallenge $authorizationChallenge): string
|
||||
{
|
||||
return $authorizationChallenge->getPayload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?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\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Challenge\ValidatorInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
|
||||
/**
|
||||
* Validator for HTTP challenges.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class HttpValidator implements ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* @var HttpDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var Client
|
||||
*/
|
||||
private $client;
|
||||
|
||||
public function __construct(HttpDataExtractor $extractor = null, Client $client = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new HttpDataExtractor();
|
||||
$this->client = $client ?: new Client();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType() && !$solver instanceof MockServerHttpSolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
|
||||
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
|
||||
|
||||
try {
|
||||
return $checkContent === trim($this->client->get($checkUrl, ['verify' => false])->getBody()->getContents());
|
||||
} catch (ClientException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?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\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Challenge\ValidatorInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* Validator for pebble-challtestsrv.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class MockHttpValidator implements ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType() && $solver instanceof MockServerHttpSolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?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\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
/**
|
||||
* ACME HTTP solver talking to pebble-challtestsrv.
|
||||
*
|
||||
* @author Titouan Galopin <galopintitouan@gmail.com>
|
||||
*/
|
||||
class MockServerHttpSolver implements SolverInterface
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
(new Client())->post('http://localhost:8055/add-http01', [
|
||||
RequestOptions::JSON => [
|
||||
'token' => $authorizationChallenge->getToken(),
|
||||
'content' => $authorizationChallenge->getPayload(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
(new Client())->post('http://localhost:8055/del-http01', [
|
||||
RequestOptions::JSON => [
|
||||
'token' => $authorizationChallenge->getToken(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
<?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\Challenge\Http;
|
||||
|
||||
use AcmePhp\Core\Challenge\SolverInterface;
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
use Symfony\Component\Console\Output\NullOutput;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
/**
|
||||
* ACME HTTP solver with manual intervention.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class SimpleHttpSolver implements SolverInterface
|
||||
{
|
||||
/**
|
||||
* @var HttpDataExtractor
|
||||
*/
|
||||
private $extractor;
|
||||
|
||||
/**
|
||||
* @var OutputInterface
|
||||
*/
|
||||
private $output;
|
||||
|
||||
public function __construct(HttpDataExtractor $extractor = null, OutputInterface $output = null)
|
||||
{
|
||||
$this->extractor = $extractor ?: new HttpDataExtractor();
|
||||
$this->output = $output ?: new NullOutput();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool
|
||||
{
|
||||
return 'http-01' === $authorizationChallenge->getType();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
|
||||
$checkContent = $this->extractor->getCheckContent($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
Create a text file accessible on URL %s
|
||||
containing the following content:
|
||||
|
||||
%s
|
||||
|
||||
Check in your browser that the URL %s returns
|
||||
the authorization token above.
|
||||
|
||||
EOF
|
||||
,
|
||||
$checkUrl,
|
||||
$checkContent,
|
||||
$checkContent
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge)
|
||||
{
|
||||
$checkUrl = $this->extractor->getCheckUrl($authorizationChallenge);
|
||||
|
||||
$this->output->writeln(
|
||||
sprintf(
|
||||
<<<'EOF'
|
||||
You can now safely remove the challenge's file at %s
|
||||
|
||||
EOF
|
||||
,
|
||||
$checkUrl
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME challenge solver able to solve several challenges at once.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface MultipleChallengesSolverInterface extends SolverInterface
|
||||
{
|
||||
/**
|
||||
* Solve the given list of authorization challenge.
|
||||
*
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*/
|
||||
public function solveAll(array $authorizationChallenges);
|
||||
|
||||
/**
|
||||
* Cleanup the environments after all challenges.
|
||||
*
|
||||
* @param AuthorizationChallenge[] $authorizationChallenges
|
||||
*/
|
||||
public function cleanupAll(array $authorizationChallenges);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?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\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME challenge solver.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface SolverInterface
|
||||
{
|
||||
/**
|
||||
* Determines whether or not the solver supports a given Challenge.
|
||||
*
|
||||
* @return bool The solver supports the given challenge's type
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge): bool;
|
||||
|
||||
/**
|
||||
* Solve the given authorization challenge.
|
||||
*/
|
||||
public function solve(AuthorizationChallenge $authorizationChallenge);
|
||||
|
||||
/**
|
||||
* Cleanup the environments after a successful challenge.
|
||||
*/
|
||||
public function cleanup(AuthorizationChallenge $authorizationChallenge);
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?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\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME challenge pre validator.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
interface ValidatorInterface
|
||||
{
|
||||
/**
|
||||
* Determines whether or not the validator supports a given Challenge.
|
||||
*
|
||||
* @return bool The validator supports the given challenge's type
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool;
|
||||
|
||||
/**
|
||||
* Internally validate the challenge by performing the same kind of test than the CA.
|
||||
*
|
||||
* @return bool The challenge is valid
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
<?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\Challenge;
|
||||
|
||||
use AcmePhp\Core\Protocol\AuthorizationChallenge;
|
||||
|
||||
/**
|
||||
* ACME Challenge validator who implement a retry strategy till the decorated validator successfully validate the
|
||||
* challenge or the timeout is reached.
|
||||
*
|
||||
* @author Jérémy Derussé <jeremy@derusse.com>
|
||||
*/
|
||||
class WaitingValidator implements ValidatorInterface
|
||||
{
|
||||
/** @var ValidatorInterface */
|
||||
private $validator;
|
||||
|
||||
/** @var int */
|
||||
private $timeout;
|
||||
|
||||
public function __construct(ValidatorInterface $validator, int $timeout = 180)
|
||||
{
|
||||
$this->validator = $validator;
|
||||
$this->timeout = $timeout;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function supports(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
return $this->validator->supports($authorizationChallenge, $solver);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isValid(AuthorizationChallenge $authorizationChallenge, SolverInterface $solver): bool
|
||||
{
|
||||
$limitEndTime = time() + $this->timeout;
|
||||
|
||||
do {
|
||||
if ($this->validator->isValid($authorizationChallenge, $solver)) {
|
||||
return true;
|
||||
}
|
||||
sleep(3);
|
||||
} while ($limitEndTime > time());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user