mirror of
https://github.com/danog/dns-over-https.git
synced 2024-12-02 09:17:50 +01:00
Switch to amp v3
This commit is contained in:
parent
a288be1f4f
commit
f4d729a758
@ -42,19 +42,21 @@
|
|||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=7.0",
|
"php": ">=7.0",
|
||||||
"amphp/cache": "^1",
|
"amphp/cache": "^v2-dev",
|
||||||
"amphp/parser": "^1",
|
"amphp/parser": "^1",
|
||||||
"danog/libdns-json": "^0.1",
|
"danog/libdns-json": "^0.1",
|
||||||
"daverandom/libdns": "^2.0.1",
|
"daverandom/libdns": "^2.0.1",
|
||||||
"amphp/amp": "^2",
|
"amphp/amp": "^v3-dev",
|
||||||
"amphp/http-client": "^4",
|
"amphp/http-client": "^v5-dev",
|
||||||
"amphp/dns": "^1",
|
"amphp/dns": "^v2-dev",
|
||||||
"ext-filter": "*",
|
"ext-filter": "*",
|
||||||
"ext-json": "*"
|
"ext-json": "*"
|
||||||
},
|
},
|
||||||
|
"minimum-stability": "dev",
|
||||||
|
"prefer-stable": true,
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"amphp/phpunit-util": "^1",
|
"amphp/phpunit-util": "^v2-dev",
|
||||||
"phpunit/phpunit": "^6",
|
"phpunit/phpunit": "^9",
|
||||||
"amphp/php-cs-fixer-config": "dev-master"
|
"amphp/php-cs-fixer-config": "dev-master"
|
||||||
},
|
},
|
||||||
"autoload": {
|
"autoload": {
|
||||||
|
@ -4,11 +4,10 @@ require __DIR__ . "/_bootstrap.php";
|
|||||||
|
|
||||||
use Amp\Dns;
|
use Amp\Dns;
|
||||||
use Amp\DoH;
|
use Amp\DoH;
|
||||||
use Amp\Loop;
|
|
||||||
|
|
||||||
print "Downloading top 500 domains..." . PHP_EOL;
|
print "Downloading top 500 domains..." . PHP_EOL;
|
||||||
|
|
||||||
$domains = \file_get_contents("https://moz.com/top-500/download/?table=top500Domains");
|
$domains = \file_get_contents("https://moz.com/top-500/download?table=top500Domains");
|
||||||
$domains = \array_map(function ($line) {
|
$domains = \array_map(function ($line) {
|
||||||
return \trim(\explode(",", $line)[1], '"/');
|
return \trim(\explode(",", $line)[1], '"/');
|
||||||
}, \array_filter(\explode("\n", $domains)));
|
}, \array_filter(\explode("\n", $domains)));
|
||||||
@ -16,32 +15,30 @@ $domains = \array_map(function ($line) {
|
|||||||
// Remove "URL" header
|
// Remove "URL" header
|
||||||
\array_shift($domains);
|
\array_shift($domains);
|
||||||
|
|
||||||
// Set default resolver to DNS-over-HTTPS resolver
|
|
||||||
$DohConfig = new DoH\DoHConfig([new DoH\Nameserver('https://mozilla.cloudflare-dns.com/dns-query')]);
|
$DohConfig = new DoH\DoHConfig([new DoH\Nameserver('https://mozilla.cloudflare-dns.com/dns-query')]);
|
||||||
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig));
|
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig));
|
||||||
|
|
||||||
Loop::run(function () use ($domains) {
|
|
||||||
print "Starting sequential queries...\r\n\r\n";
|
|
||||||
|
|
||||||
$timings = [];
|
print "Starting sequential queries...\r\n\r\n";
|
||||||
|
|
||||||
for ($i = 0; $i < 10; $i++) {
|
$timings = [];
|
||||||
$start = \microtime(1);
|
|
||||||
$domain = $domains[\random_int(0, \count($domains) - 1)];
|
|
||||||
|
|
||||||
try {
|
for ($i = 0; $i < 10; $i++) {
|
||||||
pretty_print_records($domain, yield Dns\resolve($domain));
|
$start = \microtime(1);
|
||||||
} catch (Dns\DnsException $e) {
|
$domain = $domains[\random_int(0, \count($domains) - 1)];
|
||||||
pretty_print_error($domain, $e);
|
|
||||||
}
|
|
||||||
|
|
||||||
$time = \round(\microtime(1) - $start, 2);
|
try {
|
||||||
$timings[] = $time;
|
pretty_print_records($domain, Dns\resolve($domain));
|
||||||
|
} catch (Dns\DnsException $e) {
|
||||||
\printf("%'-74s\r\n\r\n", " in " . $time . " ms");
|
pretty_print_error($domain, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
$averageTime = \array_sum($timings) / \count($timings);
|
$time = \round(\microtime(1) - $start, 2);
|
||||||
|
$timings[] = $time;
|
||||||
|
|
||||||
print "{$averageTime} ms for an average query." . PHP_EOL;
|
\printf("%'-74s\r\n\r\n", " in " . $time . " ms");
|
||||||
});
|
}
|
||||||
|
|
||||||
|
$averageTime = \array_sum($timings) / \count($timings);
|
||||||
|
|
||||||
|
print "{$averageTime} ms for an average query." . PHP_EOL;
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
require __DIR__."/_bootstrap.php";
|
require __DIR__ . "/_bootstrap.php";
|
||||||
|
|
||||||
use Amp\Dns;
|
use Amp\Dns;
|
||||||
use Amp\DoH;
|
use Amp\DoH;
|
||||||
use Amp\Loop;
|
|
||||||
use Amp\Promise;
|
|
||||||
|
|
||||||
// Used only by the subresolver for resolving the DoH nameserver URL
|
$customConfigLoader = new class implements Dns\DnsConfigLoader {
|
||||||
$customConfigLoader = new class implements Dns\ConfigLoader {
|
public function loadConfig(): Dns\DnsConfig
|
||||||
public function loadConfig(): Promise
|
|
||||||
{
|
{
|
||||||
return Amp\call(function () {
|
$hosts = (new Dns\HostLoader)->loadHosts();
|
||||||
$hosts = yield (new Dns\HostLoader)->loadHosts();
|
|
||||||
|
|
||||||
return new Dns\Config([
|
return new Dns\DnsConfig([
|
||||||
"8.8.8.8:53",
|
"8.8.8.8:53",
|
||||||
"[2001:4860:4860::8888]:53",
|
"[2001:4860:4860::8888]:53",
|
||||||
], $hosts, $timeout = 5000, $attempts = 3);
|
], $hosts, 5, 3);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
// Set default resolver to DNS-over-https resolver
|
// Set default resolver to DNS-over-https resolver
|
||||||
$DohConfig = new DoH\DoHConfig(
|
$DohConfig = new DoH\DoHConfig(
|
||||||
[
|
[
|
||||||
@ -35,12 +31,10 @@ $DohConfig = new DoH\DoHConfig(
|
|||||||
);
|
);
|
||||||
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig));
|
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig));
|
||||||
|
|
||||||
Loop::run(function () {
|
$hostname = $argv[1] ?? "amphp.org";
|
||||||
$hostname = "amphp.org";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pretty_print_records($hostname, yield Dns\resolve($hostname));
|
pretty_print_records($hostname, Dns\resolve($hostname));
|
||||||
} catch (Dns\DnsException $e) {
|
} catch (Dns\DnsException $e) {
|
||||||
pretty_print_error($hostname, $e);
|
pretty_print_error($hostname, $e);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
@ -4,18 +4,15 @@ require __DIR__ . "/_bootstrap.php";
|
|||||||
|
|
||||||
use Amp\Dns;
|
use Amp\Dns;
|
||||||
use Amp\DoH;
|
use Amp\DoH;
|
||||||
use Amp\Loop;
|
|
||||||
|
|
||||||
// Set default resolver to DNS-over-HTTPS resolver
|
// Set default resolver to DNS-over-HTTPS resolver
|
||||||
$DohConfig = new DoH\DoHConfig([new DoH\Nameserver('https://mozilla.cloudflare-dns.com/dns-query')]);
|
$DohConfig = new DoH\DoHConfig([new DoH\Nameserver('https://mozilla.cloudflare-dns.com/dns-query')]);
|
||||||
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig));
|
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig));
|
||||||
|
|
||||||
Loop::run(function () {
|
$ip = $argv[1] ?? "8.8.8.8";
|
||||||
$ip = "8.8.8.8";
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
pretty_print_records($ip, yield Dns\query($ip, Dns\Record::PTR));
|
pretty_print_records($ip, Dns\query($ip, Dns\Record::PTR));
|
||||||
} catch (Dns\DnsException $e) {
|
} catch (Dns\DnsException $e) {
|
||||||
pretty_print_error($ip, $e);
|
pretty_print_error($ip, $e);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
@ -4,6 +4,7 @@ namespace Amp\DoH;
|
|||||||
|
|
||||||
use Amp\Cache\ArrayCache;
|
use Amp\Cache\ArrayCache;
|
||||||
use Amp\Cache\Cache;
|
use Amp\Cache\Cache;
|
||||||
|
use Amp\Cache\LocalCache;
|
||||||
use Amp\Dns\ConfigException;
|
use Amp\Dns\ConfigException;
|
||||||
use Amp\Dns\ConfigLoader;
|
use Amp\Dns\ConfigLoader;
|
||||||
use Amp\Dns\Resolver;
|
use Amp\Dns\Resolver;
|
||||||
@ -15,43 +16,48 @@ use Amp\Http\Client\HttpClientBuilder;
|
|||||||
|
|
||||||
final class DoHConfig
|
final class DoHConfig
|
||||||
{
|
{
|
||||||
private $nameservers;
|
/**
|
||||||
private $httpClient;
|
* @var non-empty-array<NameServer> $nameservers
|
||||||
private $subResolver;
|
*/
|
||||||
private $configLoader;
|
private readonly array $nameservers;
|
||||||
private $cache;
|
private readonly DelegateHttpClient $httpClient;
|
||||||
|
private readonly Rfc1035StubResolver $subResolver;
|
||||||
|
private readonly ConfigLoader $configLoader;
|
||||||
|
private readonly Cache $cache;
|
||||||
|
|
||||||
public function __construct(array $nameservers, DelegateHttpClient $httpClient = null, Resolver $resolver = null, ConfigLoader $configLoader = null, Cache $cache = null)
|
/**
|
||||||
|
* @param non-empty-array<NameServer> $nameservers
|
||||||
|
*/
|
||||||
|
public function __construct(array $nameservers, ?DelegateHttpClient $httpClient = null, ?Rfc1035StubResolver $resolver = null, ?ConfigLoader $configLoader = null, ?Cache $cache = null)
|
||||||
{
|
{
|
||||||
if (\count($nameservers) < 1) {
|
if (\count($nameservers) < 1) {
|
||||||
throw new ConfigException("At least one nameserver is required for a valid config");
|
throw new ConfigException("At least one nameserver is required for a valid config");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($nameservers as $nameserver) {
|
foreach ($nameservers as $nameserver) {
|
||||||
$this->validateNameserver($nameserver);
|
if (!($nameserver instanceof Nameserver)) {
|
||||||
|
throw new ConfigException("Invalid nameserver: {$nameserver}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->nameservers = $nameservers;
|
$this->nameservers = $nameservers;
|
||||||
$this->httpClient = $httpClient ?? HttpClientBuilder::buildDefault();
|
$this->httpClient = $httpClient ?? HttpClientBuilder::buildDefault();
|
||||||
$this->cache = $cache ?? new ArrayCache(5000/* default gc interval */, 256/* size */);
|
$this->cache = $cache ?? new LocalCache(256, 5.0);
|
||||||
$this->configLoader = $configLoader ?? (\stripos(PHP_OS, "win") === 0
|
$this->configLoader = $configLoader ?? (\stripos(PHP_OS, "win") === 0
|
||||||
? new WindowsConfigLoader
|
? new WindowsConfigLoader
|
||||||
: new UnixConfigLoader);
|
: new UnixConfigLoader);
|
||||||
$this->subResolver = $resolver ?? new Rfc1035StubResolver(null, $this->configLoader);
|
$this->subResolver = $resolver ?? new Rfc1035StubResolver(null, $this->configLoader);
|
||||||
}
|
}
|
||||||
|
|
||||||
private function validateNameserver($nameserver)
|
/**
|
||||||
{
|
* @return non-empty-array<Nameserver>
|
||||||
if (!($nameserver instanceof Nameserver)) {
|
*/
|
||||||
throw new ConfigException("Invalid nameserver: {$nameserver}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getNameservers(): array
|
public function getNameservers(): array
|
||||||
{
|
{
|
||||||
return $this->nameservers;
|
return $this->nameservers;
|
||||||
}
|
}
|
||||||
public function isNameserver($string): bool
|
|
||||||
|
public function isNameserver(string $string): bool
|
||||||
{
|
{
|
||||||
foreach ($this->nameservers as $nameserver) {
|
foreach ($this->nameservers as $nameserver) {
|
||||||
if ($nameserver->getHost() === $string) {
|
if ($nameserver->getHost() === $string) {
|
||||||
@ -74,7 +80,7 @@ final class DoHConfig
|
|||||||
{
|
{
|
||||||
return $this->configLoader;
|
return $this->configLoader;
|
||||||
}
|
}
|
||||||
public function getSubResolver(): Resolver
|
public function getSubResolver(): Rfc1035StubResolver
|
||||||
{
|
{
|
||||||
return $this->subResolver;
|
return $this->subResolver;
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,8 @@
|
|||||||
namespace Amp\DoH;
|
namespace Amp\DoH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Throw when DoH resolution fails.
|
* Thrown when DoH resolution fails.
|
||||||
*/
|
*/
|
||||||
class DoHException extends \Exception
|
final class DoHException extends \Exception
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
@ -1,100 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Amp\DoH\Internal;
|
|
||||||
|
|
||||||
use Amp\DoH\DoHException;
|
|
||||||
use Amp\DoH\Nameserver;
|
|
||||||
use Amp\Http\Client\DelegateHttpClient;
|
|
||||||
use Amp\Http\Client\Request;
|
|
||||||
use Amp\Promise;
|
|
||||||
use danog\LibDNSJson\JsonDecoderFactory;
|
|
||||||
use danog\LibDNSJson\QueryEncoderFactory;
|
|
||||||
use LibDNS\Decoder\DecoderFactory;
|
|
||||||
use LibDNS\Encoder\EncoderFactory;
|
|
||||||
use LibDNS\Messages\Message;
|
|
||||||
use function Amp\call;
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
final class HttpsSocket extends Socket
|
|
||||||
{
|
|
||||||
/** @var \Amp\Http\HttpClient */
|
|
||||||
private $httpClient;
|
|
||||||
|
|
||||||
/** @var \Amp\DoH\Nameserver */
|
|
||||||
private $nameserver;
|
|
||||||
|
|
||||||
/** @var \LibDNS\Encoder\Encoder */
|
|
||||||
private $encoder;
|
|
||||||
|
|
||||||
/** @var \LibDNS\Decoder\Decoder */
|
|
||||||
private $decoder;
|
|
||||||
|
|
||||||
/** @var \Amp\Deferred */
|
|
||||||
private $responseDeferred;
|
|
||||||
|
|
||||||
public static function connect(DelegateHttpClient $httpClient, Nameserver $nameserver): Socket
|
|
||||||
{
|
|
||||||
return new self($httpClient, $nameserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function __construct(DelegateHttpClient $httpClient, Nameserver $nameserver)
|
|
||||||
{
|
|
||||||
$this->httpClient = $httpClient;
|
|
||||||
$this->nameserver = $nameserver;
|
|
||||||
|
|
||||||
if ($nameserver->getType() !== Nameserver::GOOGLE_JSON) {
|
|
||||||
$this->encoder = (new EncoderFactory)->create();
|
|
||||||
$this->decoder = (new DecoderFactory)->create();
|
|
||||||
} else {
|
|
||||||
$this->encoder = (new QueryEncoderFactory)->create();
|
|
||||||
$this->decoder = (new JsonDecoderFactory)->create();
|
|
||||||
}
|
|
||||||
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function resolve(Message $message): Promise
|
|
||||||
{
|
|
||||||
$id = $message->getID();
|
|
||||||
|
|
||||||
switch ($this->nameserver->getType()) {
|
|
||||||
case Nameserver::RFC8484_GET:
|
|
||||||
$data = $this->encoder->encode($message);
|
|
||||||
$request = new Request($this->nameserver->getUri().'?'.\http_build_query(['dns' => \base64_encode($data), 'ct' => 'application/dns-message']), "GET");
|
|
||||||
$request->setHeader('accept', 'application/dns-message');
|
|
||||||
$request->setHeaders($this->nameserver->getHeaders());
|
|
||||||
break;
|
|
||||||
case Nameserver::RFC8484_POST:
|
|
||||||
$data = $this->encoder->encode($message);
|
|
||||||
$request = new Request($this->nameserver->getUri(), "POST");
|
|
||||||
$request->setBody($data);
|
|
||||||
$request->setHeader('content-type', 'application/dns-message');
|
|
||||||
$request->setHeader('accept', 'application/dns-message');
|
|
||||||
$request->setHeader('content-length', \strlen($data));
|
|
||||||
$request->setHeaders($this->nameserver->getHeaders());
|
|
||||||
break;
|
|
||||||
case Nameserver::GOOGLE_JSON:
|
|
||||||
$data = $this->encoder->encode($message);
|
|
||||||
$request = new Request($this->nameserver->getUri().'?'.$data, "GET");
|
|
||||||
$request->setHeader('accept', 'application/dns-json');
|
|
||||||
$request->setHeaders($this->nameserver->getHeaders());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
$response = $this->httpClient->request($request);
|
|
||||||
return call(function () use ($response, $id) {
|
|
||||||
$response = yield $response;
|
|
||||||
if ($response->getStatus() !== 200) {
|
|
||||||
throw new DoHException("HTTP result !== 200: ".$response->getStatus()." ".$response->getReason(), $response->getStatus());
|
|
||||||
}
|
|
||||||
$response = yield $response->getBody()->buffer();
|
|
||||||
|
|
||||||
switch ($this->nameserver->getType()) {
|
|
||||||
case Nameserver::RFC8484_GET:
|
|
||||||
case Nameserver::RFC8484_POST:
|
|
||||||
return $this->decoder->decode($response);
|
|
||||||
case Nameserver::GOOGLE_JSON:
|
|
||||||
return $this->decoder->decode($response, $id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,198 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace Amp\DoH\Internal;
|
|
||||||
|
|
||||||
use Amp;
|
|
||||||
use Amp\ByteStream\StreamException;
|
|
||||||
use Amp\Deferred;
|
|
||||||
use Amp\Dns\DnsException;
|
|
||||||
use Amp\Dns\TimeoutException;
|
|
||||||
use Amp\DoH\DoHException;
|
|
||||||
use Amp\DoH\Nameserver;
|
|
||||||
use Amp\Http\Client\DelegateHttpClient;
|
|
||||||
use Amp\Promise;
|
|
||||||
use LibDNS\Messages\Message;
|
|
||||||
use LibDNS\Messages\MessageFactory;
|
|
||||||
use LibDNS\Messages\MessageTypes;
|
|
||||||
use LibDNS\Records\Question;
|
|
||||||
use function Amp\call;
|
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
abstract class Socket
|
|
||||||
{
|
|
||||||
const MAX_CONCURRENT_REQUESTS = 500;
|
|
||||||
|
|
||||||
/** @var array Contains already sent queries with no response yet. For UDP this is exactly zero or one item. */
|
|
||||||
private $pending = [];
|
|
||||||
|
|
||||||
/** @var MessageFactory */
|
|
||||||
private $messageFactory;
|
|
||||||
|
|
||||||
/** @var callable */
|
|
||||||
private $onResolve;
|
|
||||||
|
|
||||||
/** @var array Queued requests if the number of concurrent requests is too large. */
|
|
||||||
private $queue = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $uri
|
|
||||||
*
|
|
||||||
* @return Promise<self>
|
|
||||||
*/
|
|
||||||
|
|
||||||
abstract public static function connect(DelegateHttpClient $httpClient, Nameserver $nameserver): self;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param Message $message
|
|
||||||
*
|
|
||||||
* @return Promise<int>
|
|
||||||
*/
|
|
||||||
abstract protected function resolve(Message $message): Promise;
|
|
||||||
|
|
||||||
protected function __construct()
|
|
||||||
{
|
|
||||||
$this->messageFactory = new MessageFactory;
|
|
||||||
|
|
||||||
$this->onResolve = function (\Throwable $exception = null, Message $message = null) {
|
|
||||||
if ($exception) {
|
|
||||||
$this->error($exception);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
\assert($message instanceof Message);
|
|
||||||
|
|
||||||
$id = $message->getId();
|
|
||||||
|
|
||||||
// Ignore duplicate and invalid responses.
|
|
||||||
if (isset($this->pending[$id]) && $this->matchesQuestion($message, $this->pending[$id]->question)) {
|
|
||||||
/** @var Deferred $deferred */
|
|
||||||
$deferred = $this->pending[$id]->deferred;
|
|
||||||
unset($this->pending[$id]);
|
|
||||||
$deferred->resolve($message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param \LibDNS\Records\Question $question
|
|
||||||
* @param int $timeout
|
|
||||||
*
|
|
||||||
* @return \Amp\Promise<\LibDNS\Messages\Message>
|
|
||||||
*/
|
|
||||||
final public function ask(Question $question, int $timeout): Promise
|
|
||||||
{
|
|
||||||
return call(function () use ($question, $timeout) {
|
|
||||||
if (\count($this->pending) > self::MAX_CONCURRENT_REQUESTS) {
|
|
||||||
$deferred = new Deferred;
|
|
||||||
$this->queue[] = $deferred;
|
|
||||||
yield $deferred->promise();
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
$id = \random_int(0, 0xffff);
|
|
||||||
} while (isset($this->pending[$id]));
|
|
||||||
|
|
||||||
$deferred = new Deferred;
|
|
||||||
$pending = new class {
|
|
||||||
use Amp\Struct;
|
|
||||||
|
|
||||||
public $deferred;
|
|
||||||
public $question;
|
|
||||||
};
|
|
||||||
|
|
||||||
$pending->deferred = $deferred;
|
|
||||||
$pending->question = $question;
|
|
||||||
$this->pending[$id] = $pending;
|
|
||||||
|
|
||||||
$message = $this->createMessage($question, $id);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = $this->resolve($message);
|
|
||||||
} catch (StreamException $exception) {
|
|
||||||
$exception = new DnsException("Sending the request failed", 0, $exception);
|
|
||||||
$this->error($exception);
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
$response->onResolve($this->onResolve);
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Work around an OPCache issue that returns an empty array with "return yield ...",
|
|
||||||
// so assign to a variable first and return after the try block.
|
|
||||||
//
|
|
||||||
// See https://github.com/amphp/dns/issues/58.
|
|
||||||
// See https://bugs.php.net/bug.php?id=74840.
|
|
||||||
$result = yield Promise\timeout($deferred->promise(), $timeout);
|
|
||||||
} catch (Amp\TimeoutException $exception) {
|
|
||||||
unset($this->pending[$id]);
|
|
||||||
throw new TimeoutException("Didn't receive a response within {$timeout} milliseconds.");
|
|
||||||
} finally {
|
|
||||||
if ($this->queue) {
|
|
||||||
$deferred = \array_shift($this->queue);
|
|
||||||
$deferred->resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private function error(\Throwable $exception)
|
|
||||||
{
|
|
||||||
if (empty($this->pending)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$exception instanceof DnsException && !$exception instanceof DoHException) {
|
|
||||||
$message = "Unexpected error during resolution: ".$exception->getMessage();
|
|
||||||
$exception = new DnsException($message, 0, $exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
$pending = $this->pending;
|
|
||||||
$this->pending = [];
|
|
||||||
|
|
||||||
foreach ($pending as $pendingQuestion) {
|
|
||||||
/** @var Deferred $deferred */
|
|
||||||
$deferred = $pendingQuestion->deferred;
|
|
||||||
$deferred->fail($exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final protected function createMessage(Question $question, int $id): Message
|
|
||||||
{
|
|
||||||
$request = $this->messageFactory->create(MessageTypes::QUERY);
|
|
||||||
$request->getQuestionRecords()->add($question);
|
|
||||||
$request->isRecursionDesired(true);
|
|
||||||
$request->setID($id);
|
|
||||||
return $request;
|
|
||||||
}
|
|
||||||
|
|
||||||
private function matchesQuestion(Message $message, Question $question): bool
|
|
||||||
{
|
|
||||||
if ($message->getType() !== MessageTypes::RESPONSE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$questionRecords = $message->getQuestionRecords();
|
|
||||||
|
|
||||||
// We only ever ask one question at a time
|
|
||||||
if (\count($questionRecords) !== 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$questionRecord = $questionRecords->getIterator()->current();
|
|
||||||
|
|
||||||
if ($questionRecord->getClass() !== $question->getClass()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($questionRecord->getType() !== $question->getType()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($questionRecord->getName()->getValue() !== $question->getName()->getValue()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
@ -6,26 +6,16 @@ use Amp\Dns\ConfigException;
|
|||||||
|
|
||||||
final class Nameserver
|
final class Nameserver
|
||||||
{
|
{
|
||||||
const RFC8484_GET = 0;
|
private readonly string $host;
|
||||||
const RFC8484_POST = 1;
|
|
||||||
const GOOGLE_JSON = 2;
|
|
||||||
|
|
||||||
private $type;
|
public function __construct(
|
||||||
private $uri;
|
private readonly string $uri,
|
||||||
private $host;
|
private readonly NameserverType $type = NameserverType::RFC8484_POST,
|
||||||
private $headers = [];
|
private readonly array $headers = []
|
||||||
|
) {
|
||||||
public function __construct(string $uri, int $type = self::RFC8484_POST, array $headers = [])
|
|
||||||
{
|
|
||||||
if (\parse_url($uri, PHP_URL_SCHEME) !== 'https') {
|
if (\parse_url($uri, PHP_URL_SCHEME) !== 'https') {
|
||||||
throw new ConfigException('Did not provide a valid HTTPS url!');
|
throw new ConfigException('Did not provide a valid HTTPS url!');
|
||||||
}
|
}
|
||||||
if (!\in_array($type, [self::RFC8484_GET, self::RFC8484_POST, self::GOOGLE_JSON])) {
|
|
||||||
throw new ConfigException('Invalid nameserver type provided!');
|
|
||||||
}
|
|
||||||
$this->uri = $uri;
|
|
||||||
$this->type = $type;
|
|
||||||
$this->headers = $headers;
|
|
||||||
$this->host = \parse_url($uri, PHP_URL_HOST);
|
$this->host = \parse_url($uri, PHP_URL_HOST);
|
||||||
}
|
}
|
||||||
public function getUri(): string
|
public function getUri(): string
|
||||||
@ -40,21 +30,12 @@ final class Nameserver
|
|||||||
{
|
{
|
||||||
return $this->headers;
|
return $this->headers;
|
||||||
}
|
}
|
||||||
public function getType(): int
|
public function getType(): NameserverType
|
||||||
{
|
{
|
||||||
return $this->type;
|
return $this->type;
|
||||||
}
|
}
|
||||||
public function __toString(): string
|
public function __toString(): string
|
||||||
{
|
{
|
||||||
return $this->uri;
|
return $this->uri;
|
||||||
/*
|
|
||||||
switch ($this->type) {
|
|
||||||
case self::RFC8484_GET:
|
|
||||||
return "{$this->uri} RFC 8484 GET";
|
|
||||||
case self::RFC8484_POST:
|
|
||||||
return "{$this->uri} RFC 8484 POST";
|
|
||||||
case self::GOOGLE_JSON:
|
|
||||||
return "{$this->uri} google JSON";
|
|
||||||
}*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
lib/NameserverType.php
Normal file
12
lib/NameserverType.php
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Amp\DoH;
|
||||||
|
|
||||||
|
use Amp\Dns\ConfigException;
|
||||||
|
|
||||||
|
enum NameserverType
|
||||||
|
{
|
||||||
|
case RFC8484_GET;
|
||||||
|
case RFC8484_POST;
|
||||||
|
case GOOGLE_JSON;
|
||||||
|
}
|
@ -3,208 +3,203 @@
|
|||||||
namespace Amp\DoH;
|
namespace Amp\DoH;
|
||||||
|
|
||||||
use Amp\Cache\Cache;
|
use Amp\Cache\Cache;
|
||||||
|
use Amp\Cancellation;
|
||||||
|
use Amp\CompositeException;
|
||||||
use Amp\Dns\Config;
|
use Amp\Dns\Config;
|
||||||
use Amp\Dns\ConfigException;
|
use Amp\Dns\ConfigException;
|
||||||
|
use Amp\Dns\ConfigLoader;
|
||||||
use Amp\Dns\DnsException;
|
use Amp\Dns\DnsException;
|
||||||
use Amp\Dns\NoRecordException;
|
use Amp\Dns\NoRecordException;
|
||||||
use Amp\Dns\Record;
|
use Amp\Dns\Record;
|
||||||
use Amp\Dns\Resolver;
|
use Amp\Dns\Resolver;
|
||||||
|
use Amp\Dns\Rfc1035StubResolver;
|
||||||
use Amp\Dns\TimeoutException;
|
use Amp\Dns\TimeoutException;
|
||||||
use Amp\DoH\Internal\HttpsSocket;
|
use Amp\Future;
|
||||||
use Amp\DoH\Internal\Socket;
|
use Amp\Http\Client\DelegateHttpClient;
|
||||||
|
use Amp\Http\Client\Request;
|
||||||
use Amp\MultiReasonException;
|
use Amp\MultiReasonException;
|
||||||
|
use Amp\NullCancellation;
|
||||||
use Amp\Promise;
|
use Amp\Promise;
|
||||||
|
use danog\LibDNSJson\JsonDecoder;
|
||||||
|
use danog\LibDNSJson\JsonDecoderFactory;
|
||||||
|
use danog\LibDNSJson\QueryEncoder;
|
||||||
|
use danog\LibDNSJson\QueryEncoderFactory;
|
||||||
|
use LibDNS\Decoder\Decoder;
|
||||||
|
use LibDNS\Decoder\DecoderFactory;
|
||||||
|
use LibDNS\Encoder\Encoder;
|
||||||
|
use LibDNS\Encoder\EncoderFactory;
|
||||||
use LibDNS\Messages\Message;
|
use LibDNS\Messages\Message;
|
||||||
|
use LibDNS\Messages\MessageFactory;
|
||||||
|
use LibDNS\Messages\MessageTypes;
|
||||||
use LibDNS\Records\Question;
|
use LibDNS\Records\Question;
|
||||||
use LibDNS\Records\QuestionFactory;
|
use LibDNS\Records\QuestionFactory;
|
||||||
|
|
||||||
|
use function Amp\async;
|
||||||
use function Amp\call;
|
use function Amp\call;
|
||||||
use function Amp\Dns\normalizeName;
|
use function Amp\Dns\normalizeName;
|
||||||
|
use function Amp\Future\awaitAll;
|
||||||
|
|
||||||
final class Rfc8484StubResolver implements Resolver
|
final class Rfc8484StubResolver implements Resolver
|
||||||
{
|
{
|
||||||
const CACHE_PREFIX = "amphp.doh.";
|
const CACHE_PREFIX = "amphp.doh.";
|
||||||
|
|
||||||
/** @var \Amp\Dns\ConfigLoader */
|
private ConfigLoader $configLoader;
|
||||||
private $configLoader;
|
private QuestionFactory $questionFactory;
|
||||||
|
private ?Config $config = null;
|
||||||
|
|
||||||
/** @var \LibDNS\Records\QuestionFactory */
|
private ?Future $pendingConfig = null;
|
||||||
private $questionFactory;
|
|
||||||
|
|
||||||
/** @var \Amp\Dns\Config|null */
|
private Cache $cache;
|
||||||
private $config;
|
|
||||||
|
|
||||||
/** @var Promise|null */
|
|
||||||
private $pendingConfig;
|
|
||||||
|
|
||||||
/** @var \Amp\DoH\DoHConfig */
|
|
||||||
private $dohConfig;
|
|
||||||
|
|
||||||
/** @var Cache */
|
|
||||||
private $cache;
|
|
||||||
|
|
||||||
/** @var Promise[] */
|
/** @var Promise[] */
|
||||||
private $pendingQueries = [];
|
private array $pendingQueries = [];
|
||||||
|
|
||||||
/** @var \Amp\Dns\Rfc1035StubResolver */
|
private Rfc1035StubResolver $subResolver;
|
||||||
private $subResolver;
|
private Encoder $encoder;
|
||||||
|
private Decoder $decoder;
|
||||||
|
private QueryEncoder $encoderJson;
|
||||||
|
private JsonDecoder $decoderJson;
|
||||||
|
private MessageFactory $messageFactory;
|
||||||
|
private DelegateHttpClient $httpClient;
|
||||||
|
|
||||||
public function __construct(DoHConfig $config)
|
public function __construct(private DoHConfig $dohConfig)
|
||||||
{
|
{
|
||||||
$resolver = $config->getSubResolver();
|
$this->cache = $dohConfig->getCache();
|
||||||
if ($resolver instanceof Rfc8484StubResolver) {
|
$this->configLoader = $dohConfig->getConfigLoader();
|
||||||
throw new ConfigException("Can't use Rfc8484StubResolver as subresolver for Rfc8484StubResolver");
|
$this->subResolver = $dohConfig->getSubResolver();
|
||||||
}
|
|
||||||
|
|
||||||
$this->cache = $config->getCache();
|
|
||||||
$this->configLoader = $config->getConfigLoader();
|
|
||||||
$this->subResolver = $resolver;
|
|
||||||
$this->dohConfig = $config;
|
|
||||||
$this->questionFactory = new QuestionFactory;
|
$this->questionFactory = new QuestionFactory;
|
||||||
|
$this->encoder = (new EncoderFactory)->create();
|
||||||
|
$this->decoder = (new DecoderFactory)->create();
|
||||||
|
$this->encoderJson = (new QueryEncoderFactory)->create();
|
||||||
|
$this->decoderJson = (new JsonDecoderFactory)->create();
|
||||||
|
$this->httpClient = $dohConfig->getHttpClient();
|
||||||
|
$this->messageFactory = new MessageFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
public function resolve(string $name, int $typeRestriction = null): Promise
|
public function resolve(string $name, int $typeRestriction = null, ?Cancellation $cancellation = null): array
|
||||||
{
|
{
|
||||||
if ($typeRestriction !== null && $typeRestriction !== Record::A && $typeRestriction !== Record::AAAA) {
|
if ($typeRestriction !== null && $typeRestriction !== Record::A && $typeRestriction !== Record::AAAA) {
|
||||||
throw new \Error("Invalid value for parameter 2: null|Record::A|Record::AAAA expected");
|
throw new \Error("Invalid value for parameter 2: null|Record::A|Record::AAAA expected");
|
||||||
}
|
}
|
||||||
|
|
||||||
return call(function () use ($name, $typeRestriction) {
|
if (!$this->config) {
|
||||||
if (!$this->config) {
|
try {
|
||||||
try {
|
$this->reloadConfig();
|
||||||
yield $this->reloadConfig();
|
} catch (ConfigException $e) {
|
||||||
} catch (ConfigException $e) {
|
$this->config = new Config(['0.0.0.0'], []);
|
||||||
$this->config = new Config(['0.0.0.0'], []);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ($typeRestriction) {
|
||||||
|
case Record::A:
|
||||||
|
if (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return [new Record($name, Record::A, null)];
|
||||||
|
} elseif (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
throw new DnsException("Got an IPv6 address, but type is restricted to IPv4");
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
|
case Record::AAAA:
|
||||||
|
if (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return [new Record($name, Record::AAAA, null)];
|
||||||
|
} elseif (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
throw new DnsException("Got an IPv4 address, but type is restricted to IPv6");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
||||||
|
return [new Record($name, Record::A, null)];
|
||||||
|
} elseif (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
||||||
|
return [new Record($name, Record::AAAA, null)];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
switch ($typeRestriction) {
|
$name = normalizeName($name);
|
||||||
case Record::A:
|
|
||||||
if (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
||||||
return [new Record($name, Record::A, null)];
|
|
||||||
} elseif (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
||||||
throw new DnsException("Got an IPv6 address, but type is restricted to IPv4");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case Record::AAAA:
|
|
||||||
if (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
||||||
return [new Record($name, Record::AAAA, null)];
|
|
||||||
} elseif (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
||||||
throw new DnsException("Got an IPv4 address, but type is restricted to IPv6");
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) {
|
|
||||||
return [new Record($name, Record::A, null)];
|
|
||||||
} elseif (\filter_var($name, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
|
|
||||||
return [new Record($name, Record::AAAA, null)];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = normalizeName($name);
|
if ($records = $this->queryHosts($name, $typeRestriction)) {
|
||||||
|
return $records;
|
||||||
|
}
|
||||||
|
|
||||||
if ($records = $this->queryHosts($name, $typeRestriction)) {
|
// Follow RFC 6761 and never send queries for localhost to the caching DNS server
|
||||||
return $records;
|
// Usually, these queries are already resolved via queryHosts()
|
||||||
}
|
if ($name === 'localhost') {
|
||||||
|
return $typeRestriction === Record::AAAA
|
||||||
// Follow RFC 6761 and never send queries for localhost to the caching DNS server
|
|
||||||
// Usually, these queries are already resolved via queryHosts()
|
|
||||||
if ($name === 'localhost') {
|
|
||||||
return $typeRestriction === Record::AAAA
|
|
||||||
? [new Record('::1', Record::AAAA, null)]
|
? [new Record('::1', Record::AAAA, null)]
|
||||||
: [new Record('127.0.0.1', Record::A, null)];
|
: [new Record('127.0.0.1', Record::A, null)];
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->dohConfig->isNameserver($name)) {
|
if ($this->dohConfig->isNameserver($name)) {
|
||||||
// Work around an OPCache issue that returns an empty array with "return yield ...",
|
return $this->subResolver->resolve($name, $typeRestriction, $cancellation);
|
||||||
// so assign to a variable first and return after the try block.
|
}
|
||||||
//
|
|
||||||
// See https://github.com/amphp/dns/issues/58.
|
|
||||||
// See https://bugs.php.net/bug.php?id=74840.
|
|
||||||
|
|
||||||
$records = yield $this->subResolver->resolve($name, $typeRestriction);
|
for ($redirects = 0; $redirects < 5; $redirects++) {
|
||||||
return $records;
|
try {
|
||||||
}
|
if ($typeRestriction) {
|
||||||
|
return $this->query($name, $typeRestriction, $cancellation);
|
||||||
|
}
|
||||||
|
list($exceptions, $records) = awaitAll([
|
||||||
|
async(fn () => $this->query($name, Record::A, $cancellation)),
|
||||||
|
async(fn () => $this->query($name, Record::AAAA, $cancellation)),
|
||||||
|
]);
|
||||||
|
|
||||||
for ($redirects = 0; $redirects < 5; $redirects++) {
|
if (\count($exceptions) === 2) {
|
||||||
try {
|
$errors = [];
|
||||||
if ($typeRestriction) {
|
|
||||||
$records = yield $this->query($name, $typeRestriction);
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
list(, $records) = yield Promise\some([
|
|
||||||
$this->query($name, Record::A),
|
|
||||||
$this->query($name, Record::AAAA),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$records = \array_merge(...$records);
|
foreach ($exceptions as $reason) {
|
||||||
break; // Break redirect loop, otherwise we query the same records 5 times
|
if ($reason instanceof NoRecordException) {
|
||||||
} catch (MultiReasonException $e) {
|
throw $reason;
|
||||||
$errors = [];
|
|
||||||
|
|
||||||
foreach ($e->getReasons() as $reason) {
|
|
||||||
if ($reason instanceof NoRecordException) {
|
|
||||||
throw $reason;
|
|
||||||
}
|
|
||||||
$error = (string) $reason;//->getMessage();
|
|
||||||
if ($reason instanceof MultiReasonException) {
|
|
||||||
$reasons = [];
|
|
||||||
foreach ($reason->getReasons() as $reason) {
|
|
||||||
$reasons []= (string) $reason;//->getMessage();
|
|
||||||
}
|
|
||||||
$error .= " (".\implode(", ", $reasons).")";
|
|
||||||
}
|
|
||||||
$errors[] = $error;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new DnsException("All query attempts failed for {$name}: ".\implode(", ", $errors), 0, $e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($searchIndex < \count($searchList) - 1 && \in_array($reason->getCode(), [2, 3], true)) {
|
||||||
|
continue 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
$errors[] = $reason->getMessage();
|
||||||
}
|
}
|
||||||
} catch (NoRecordException $e) {
|
|
||||||
try {
|
throw new DnsException(
|
||||||
/** @var Record[] $cnameRecords */
|
"All query attempts failed for {$name}: " . \implode(", ", $errors),
|
||||||
$cnameRecords = yield $this->query($name, Record::CNAME);
|
0,
|
||||||
$name = $cnameRecords[0]->getValue();
|
new CompositeException($exceptions)
|
||||||
continue;
|
);
|
||||||
} catch (NoRecordException $e) {
|
}
|
||||||
/** @var Record[] $dnameRecords */
|
return \array_merge(...$records);
|
||||||
$dnameRecords = yield $this->query($name, Record::DNAME);
|
} catch (NoRecordException $e) {
|
||||||
$name = $dnameRecords[0]->getValue();
|
try {
|
||||||
continue;
|
$cnameRecords = $this->query($name, Record::CNAME, $cancellation);
|
||||||
}
|
$name = $cnameRecords[0]->getValue();
|
||||||
|
continue;
|
||||||
|
} catch (NoRecordException) {
|
||||||
|
$dnameRecords = $this->query($name, Record::DNAME, $cancellation);
|
||||||
|
$name = $dnameRecords[0]->getValue();
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $records;
|
return $records;
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reloads the configuration in the background.
|
* Reloads the configuration in the background.
|
||||||
*
|
*
|
||||||
* Once it's finished, the configuration will be used for new requests.
|
* Once it's finished, the configuration will be used for new requests.
|
||||||
*
|
|
||||||
* @return Promise
|
|
||||||
*/
|
*/
|
||||||
public function reloadConfig(): Promise
|
public function reloadConfig(): void
|
||||||
{
|
{
|
||||||
if ($this->pendingConfig) {
|
if (!$this->pendingConfig) {
|
||||||
return $this->pendingConfig;
|
$this->pendingConfig = async(function () {
|
||||||
|
try {
|
||||||
|
$this->subResolver->reloadConfig();
|
||||||
|
$this->config = $this->configLoader->loadConfig();
|
||||||
|
} finally {
|
||||||
|
$this->pendingConfig = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$promise = call(function () {
|
$this->pendingConfig->await();
|
||||||
yield $this->subResolver->reloadConfig();
|
|
||||||
$this->config = yield $this->configLoader->loadConfig();
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->pendingConfig = $promise;
|
|
||||||
|
|
||||||
$promise->onResolve(function () {
|
|
||||||
$this->pendingConfig = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
return $promise;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private function queryHosts(string $name, int $typeRestriction = null): array
|
private function queryHosts(string $name, int $typeRestriction = null): array
|
||||||
@ -227,128 +222,142 @@ final class Rfc8484StubResolver implements Resolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
public function query(string $name, int $type): Promise
|
public function query(string $name, int $type, ?Cancellation $cancellation = null): array
|
||||||
{
|
{
|
||||||
|
$cancellation ??= new NullCancellation;
|
||||||
$pendingQueryKey = $type." ".$name;
|
$pendingQueryKey = $type." ".$name;
|
||||||
|
|
||||||
if (isset($this->pendingQueries[$pendingQueryKey])) {
|
if (isset($this->pendingQueries[$pendingQueryKey])) {
|
||||||
return $this->pendingQueries[$pendingQueryKey];
|
return $this->pendingQueries[$pendingQueryKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
$promise = call(function () use ($name, $type) {
|
$promise = async(function () use ($name, $type, $cancellation, $pendingQueryKey) {
|
||||||
if (!$this->config) {
|
try {
|
||||||
try {
|
if (!$this->config) {
|
||||||
yield $this->reloadConfig();
|
|
||||||
} catch (ConfigException $e) {
|
|
||||||
$this->config = new Config(['0.0.0.0'], []);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = $this->normalizeName($name, $type);
|
|
||||||
$question = $this->createQuestion($name, $type);
|
|
||||||
|
|
||||||
if (null !== $cachedValue = yield $this->cache->get($this->getCacheKey($name, $type))) {
|
|
||||||
return $this->decodeCachedResult($name, $type, $cachedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @var Nameserver[] $nameservers */
|
|
||||||
$nameservers = $this->dohConfig->getNameservers();
|
|
||||||
$attempts = $this->config->getAttempts() * \count($nameservers);
|
|
||||||
$attempt = 0;
|
|
||||||
|
|
||||||
/** @var Socket $socket */
|
|
||||||
$nameserver = $nameservers[0];
|
|
||||||
$socket = $this->getSocket($nameserver);
|
|
||||||
|
|
||||||
$attemptDescription = [];
|
|
||||||
|
|
||||||
$exceptions = [];
|
|
||||||
|
|
||||||
while ($attempt < $attempts) {
|
|
||||||
try {
|
|
||||||
$attemptDescription[] = $nameserver;
|
|
||||||
|
|
||||||
/** @var Message $response */
|
|
||||||
try {
|
try {
|
||||||
$response = yield $socket->ask($question, $this->config->getTimeout());
|
$this->reloadConfig();
|
||||||
} catch (DoHException $e) {
|
} catch (ConfigException $e) {
|
||||||
// Defer call, because it might interfere with the unreference() call in Internal\Socket otherwise
|
$this->config = new Config(['0.0.0.0'], []);
|
||||||
$exceptions []= $e;
|
|
||||||
|
|
||||||
$i = ++$attempt % \count($nameservers);
|
|
||||||
$nameserver = $nameservers[$i];
|
|
||||||
$socket = $this->getSocket($nameserver);
|
|
||||||
continue;
|
|
||||||
} catch (NoRecordException $e) {
|
|
||||||
// Defer call, because it might interfere with the unreference() call in Internal\Socket otherwise
|
|
||||||
|
|
||||||
$i = ++$attempt % \count($nameservers);
|
|
||||||
$nameserver = $nameservers[$i];
|
|
||||||
$socket = $this->getSocket($nameserver);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
$this->assertAcceptableResponse($response);
|
|
||||||
|
|
||||||
if ($response->isTruncated()) {
|
|
||||||
throw new DnsException("Server returned a truncated response for '{$name}' (".Record::getName($type).")");
|
|
||||||
}
|
|
||||||
|
|
||||||
$answers = $response->getAnswerRecords();
|
|
||||||
$result = [];
|
|
||||||
$ttls = [];
|
|
||||||
|
|
||||||
/** @var \LibDNS\Records\Resource $record */
|
|
||||||
foreach ($answers as $record) {
|
|
||||||
$recordType = $record->getType();
|
|
||||||
$result[$recordType][] = (string) $record->getData();
|
|
||||||
|
|
||||||
// Cache for max one day
|
|
||||||
$ttls[$recordType] = \min($ttls[$recordType] ?? 86400, $record->getTTL());
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($result as $recordType => $records) {
|
|
||||||
// We don't care here whether storing in the cache fails
|
|
||||||
$this->cache->set($this->getCacheKey($name, $recordType), \json_encode($records), $ttls[$recordType]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isset($result[$type])) {
|
|
||||||
// "it MUST NOT cache it for longer than five (5) minutes" per RFC 2308 section 7.1
|
|
||||||
$this->cache->set($this->getCacheKey($name, $type), \json_encode([]), 300);
|
|
||||||
throw new NoRecordException("No records returned for '{$name}' (".Record::getName($type).")");
|
|
||||||
}
|
|
||||||
|
|
||||||
return \array_map(function ($data) use ($type, $ttls) {
|
|
||||||
return new Record($data, $type, $ttls[$type]);
|
|
||||||
}, $result[$type]);
|
|
||||||
} catch (TimeoutException $e) {
|
|
||||||
// Defer call, because it might interfere with the unreference() call in Internal\Socket otherwise
|
|
||||||
|
|
||||||
$i = ++$attempt % \count($nameservers);
|
|
||||||
$nameserver = $nameservers[$i];
|
|
||||||
$socket = $this->getSocket($nameserver);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
$timeout = new TimeoutException(\sprintf(
|
$name = $this->normalizeName($name, $type);
|
||||||
"No response for '%s' (%s) from any nameserver after %d attempts, tried %s",
|
$question = $this->createQuestion($name, $type);
|
||||||
$name,
|
|
||||||
Record::getName($type),
|
if (null !== $cachedValue = $this->cache->get($this->getCacheKey($name, $type))) {
|
||||||
$attempts,
|
return $this->decodeCachedResult($name, $type, $cachedValue);
|
||||||
\implode(", ", $attemptDescription)
|
}
|
||||||
));
|
|
||||||
if (!$exceptions) {
|
$nameservers = $this->dohConfig->getNameservers();
|
||||||
throw $timeout;
|
$attempts = $this->config->getAttempts() * \count($nameservers);
|
||||||
|
$attempt = 0;
|
||||||
|
|
||||||
|
$nameserver = $nameservers[0];
|
||||||
|
|
||||||
|
$attemptDescription = [];
|
||||||
|
|
||||||
|
while ($attempt < $attempts) {
|
||||||
|
try {
|
||||||
|
$attemptDescription[] = $nameserver;
|
||||||
|
|
||||||
|
$response = $this->ask($nameserver, $question, $cancellation);
|
||||||
|
$this->assertAcceptableResponse($response);
|
||||||
|
|
||||||
|
if ($response->isTruncated()) {
|
||||||
|
throw new DnsException("Server returned a truncated response for '{$name}' (".Record::getName($type).")");
|
||||||
|
}
|
||||||
|
|
||||||
|
$answers = $response->getAnswerRecords();
|
||||||
|
$result = [];
|
||||||
|
$ttls = [];
|
||||||
|
|
||||||
|
/** @var \LibDNS\Records\Resource $record */
|
||||||
|
foreach ($answers as $record) {
|
||||||
|
$recordType = $record->getType();
|
||||||
|
$result[$recordType][] = (string) $record->getData();
|
||||||
|
|
||||||
|
// Cache for max one day
|
||||||
|
$ttls[$recordType] = \min($ttls[$recordType] ?? 86400, $record->getTTL());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($result as $recordType => $records) {
|
||||||
|
// We don't care here whether storing in the cache fails
|
||||||
|
$this->cache->set($this->getCacheKey($name, $recordType), \json_encode($records), $ttls[$recordType]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($result[$type])) {
|
||||||
|
// "it MUST NOT cache it for longer than five (5) minutes" per RFC 2308 section 7.1
|
||||||
|
$this->cache->set($this->getCacheKey($name, $type), \json_encode([]), 300);
|
||||||
|
throw new NoRecordException("No records returned for '{$name}' (".Record::getName($type).")");
|
||||||
|
}
|
||||||
|
|
||||||
|
return \array_map(function ($data) use ($type, $ttls) {
|
||||||
|
return new Record($data, $type, $ttls[$type]);
|
||||||
|
}, $result[$type]);
|
||||||
|
} catch (TimeoutException) {
|
||||||
|
$i = ++$attempt % \count($nameservers);
|
||||||
|
$nameserver = $nameservers[$i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException(\sprintf(
|
||||||
|
"No response for '%s' (%s) from any nameserver within %d ms after %d attempts, tried %s",
|
||||||
|
$name,
|
||||||
|
Record::getName($type),
|
||||||
|
$this->config->getTimeout(),
|
||||||
|
$attempts,
|
||||||
|
\implode(", ", $attemptDescription)
|
||||||
|
));
|
||||||
|
} finally {
|
||||||
|
unset($this->pendingQueries[$pendingQueryKey]);
|
||||||
}
|
}
|
||||||
throw new MultiReasonException($exceptions, $timeout->getMessage());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$this->pendingQueries[$type." ".$name] = $promise;
|
$this->pendingQueries[$pendingQueryKey] = $promise;
|
||||||
$promise->onResolve(function () use ($name, $type) {
|
|
||||||
unset($this->pendingQueries[$type." ".$name]);
|
|
||||||
});
|
|
||||||
|
|
||||||
return $promise;
|
return $promise->await($cancellation);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ask(Nameserver $nameserver, Question $question, Cancellation $cancellation): Message
|
||||||
|
{
|
||||||
|
$message = $this->createMessage($question, \random_int(0, 0xffff));
|
||||||
|
switch ($nameserver->getType()) {
|
||||||
|
case NameserverType::RFC8484_GET:
|
||||||
|
$data = $this->encoder->encode($message);
|
||||||
|
$request = new Request($nameserver->getUri().'?'.\http_build_query(['dns' => \base64_encode($data), 'ct' => 'application/dns-message']), "GET");
|
||||||
|
$request->setHeader('accept', 'application/dns-message');
|
||||||
|
$request->setHeaders($nameserver->getHeaders());
|
||||||
|
break;
|
||||||
|
case NameserverType::RFC8484_POST:
|
||||||
|
$data = $this->encoder->encode($message);
|
||||||
|
$request = new Request($nameserver->getUri(), "POST");
|
||||||
|
$request->setBody($data);
|
||||||
|
$request->setHeader('content-type', 'application/dns-message');
|
||||||
|
$request->setHeader('accept', 'application/dns-message');
|
||||||
|
$request->setHeader('content-length', \strlen($data));
|
||||||
|
$request->setHeaders($nameserver->getHeaders());
|
||||||
|
break;
|
||||||
|
case NameserverType::GOOGLE_JSON:
|
||||||
|
$data = $this->encoderJson->encode($message);
|
||||||
|
$request = new Request($nameserver->getUri().'?'.$data, "GET");
|
||||||
|
$request->setHeader('accept', 'application/dns-json');
|
||||||
|
$request->setHeaders($nameserver->getHeaders());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = $this->httpClient->request($request, $cancellation);
|
||||||
|
if ($response->getStatus() !== 200) {
|
||||||
|
throw new DoHException("HTTP result !== 200: ".$response->getStatus()." ".$response->getReason(), $response->getStatus());
|
||||||
|
}
|
||||||
|
$response = $response->getBody()->buffer();
|
||||||
|
|
||||||
|
switch ($nameserver->getType()) {
|
||||||
|
case NameserverType::RFC8484_GET:
|
||||||
|
case NameserverType::RFC8484_POST:
|
||||||
|
return $this->decoder->decode($response);
|
||||||
|
case NameserverType::GOOGLE_JSON:
|
||||||
|
return $this->decoderJson->decode($response, $message->getID());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private function normalizeName(string $name, int $type)
|
private function normalizeName(string $name, int $type)
|
||||||
@ -368,12 +377,6 @@ final class Rfc8484StubResolver implements Resolver
|
|||||||
return $name;
|
return $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $name
|
|
||||||
* @param int $type
|
|
||||||
*
|
|
||||||
* @return \LibDNS\Records\Question
|
|
||||||
*/
|
|
||||||
private function createQuestion(string $name, int $type): Question
|
private function createQuestion(string $name, int $type): Question
|
||||||
{
|
{
|
||||||
if (0 > $type || 0xffff < $type) {
|
if (0 > $type || 0xffff < $type) {
|
||||||
@ -387,6 +390,15 @@ final class Rfc8484StubResolver implements Resolver
|
|||||||
return $question;
|
return $question;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function createMessage(Question $question, int $id): Message
|
||||||
|
{
|
||||||
|
$request = $this->messageFactory->create(MessageTypes::QUERY);
|
||||||
|
$request->getQuestionRecords()->add($question);
|
||||||
|
$request->isRecursionDesired(true);
|
||||||
|
$request->setID($id);
|
||||||
|
return $request;
|
||||||
|
}
|
||||||
|
|
||||||
private function getCacheKey(string $name, int $type): string
|
private function getCacheKey(string $name, int $type): string
|
||||||
{
|
{
|
||||||
return self::CACHE_PREFIX.$name."#".$type;
|
return self::CACHE_PREFIX.$name."#".$type;
|
||||||
@ -409,19 +421,7 @@ final class Rfc8484StubResolver implements Resolver
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getSocket(Nameserver $nameserver)
|
private function assertAcceptableResponse(Message $response): void
|
||||||
{
|
|
||||||
$uri = $nameserver->getUri();
|
|
||||||
if (isset($this->sockets[$uri])) {
|
|
||||||
return $this->sockets[$uri];
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->sockets[$uri] = HttpsSocket::connect($this->dohConfig->getHttpClient(), $nameserver);
|
|
||||||
|
|
||||||
return $this->sockets[$uri];
|
|
||||||
}
|
|
||||||
|
|
||||||
private function assertAcceptableResponse(Message $response)
|
|
||||||
{
|
{
|
||||||
if ($response->getResponseCode() !== 0) {
|
if ($response->getResponseCode() !== 0) {
|
||||||
throw new DnsException(\sprintf("Server returned error code: %d", $response->getResponseCode()));
|
throw new DnsException(\sprintf("Server returned error code: %d", $response->getResponseCode()));
|
||||||
|
Loading…
Reference in New Issue
Block a user