mirror of
https://github.com/danog/dns-over-https.git
synced 2024-11-26 11:54:44 +01:00
First commit
This commit is contained in:
commit
6f0b89aa75
10
.gitattributes
vendored
Normal file
10
.gitattributes
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
/test export-ignore
|
||||
/examples export-ignore
|
||||
/.coveralls.yml export-ignore
|
||||
/.editorconfig export-ignore
|
||||
/.gitattributes export-ignore
|
||||
/.gitignore export-ignore
|
||||
/.php_cs export-ignore
|
||||
/.travis.yml export-ignore
|
||||
/phpunit.xml export-ignore
|
||||
docs/asset export-ignore
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
build
|
||||
composer.lock
|
||||
phpunit.xml
|
||||
vendor
|
||||
.php_cs.cache
|
||||
coverage
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "docs/.shared"]
|
||||
path = docs/.shared
|
||||
url = https://github.com/amphp/amphp.github.io
|
13
.php_cs.dist
Normal file
13
.php_cs.dist
Normal file
@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
$config = new Amp\CodeStyle\Config();
|
||||
$config->getFinder()
|
||||
->in(__DIR__ . '/examples')
|
||||
->in(__DIR__ . '/lib')
|
||||
->in(__DIR__ . '/test');
|
||||
|
||||
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
|
||||
|
||||
$config->setCacheFile($cacheDir . '/.php_cs.cache');
|
||||
|
||||
return $config;
|
39
.travis.yml
Normal file
39
.travis.yml
Normal file
@ -0,0 +1,39 @@
|
||||
sudo: false
|
||||
|
||||
language: php
|
||||
|
||||
php:
|
||||
- 7.0
|
||||
- 7.1
|
||||
- 7.2
|
||||
- 7.3
|
||||
- nightly
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- php: nightly
|
||||
fast_finish: true
|
||||
|
||||
env:
|
||||
- AMP_DEBUG=true
|
||||
|
||||
before_install:
|
||||
- phpenv config-rm xdebug.ini || echo "No xdebug config."
|
||||
|
||||
install:
|
||||
- composer update -n --prefer-dist
|
||||
- wget https://github.com/php-coveralls/php-coveralls/releases/download/v1.0.2/coveralls.phar
|
||||
- chmod +x coveralls.phar
|
||||
|
||||
script:
|
||||
- vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
|
||||
- PHP_CS_FIXER_IGNORE_ENV=1 php vendor/bin/php-cs-fixer --diff --dry-run -v fix
|
||||
|
||||
after_script:
|
||||
- ./coveralls.phar -v
|
||||
|
||||
cache:
|
||||
directories:
|
||||
- $HOME/.composer/cache
|
||||
- $HOME/.php-cs-fixer
|
||||
- $HOME/.local
|
30
CONTRIBUTING.md
Normal file
30
CONTRIBUTING.md
Normal file
@ -0,0 +1,30 @@
|
||||
## Submitting useful bug reports
|
||||
|
||||
Please search existing issues first to make sure this is not a duplicate.
|
||||
Every issue report has a cost for the developers required to field it; be
|
||||
respectful of others' time and ensure your report isn't spurious prior to
|
||||
submission. Please adhere to [sound bug reporting principles](http://www.chiark.greenend.org.uk/~sgtatham/bugs.html).
|
||||
|
||||
## Development ideology
|
||||
|
||||
Truths which we believe to be self-evident:
|
||||
|
||||
- **It's an asynchronous world.** Be wary of anything that undermines
|
||||
async principles.
|
||||
|
||||
- **The answer is not more options.** If you feel compelled to expose
|
||||
new preferences to the user it's very possible you've made a wrong
|
||||
turn somewhere.
|
||||
|
||||
- **There are no power users.** The idea that some users "understand"
|
||||
concepts better than others has proven to be, for the most part, false.
|
||||
If anything, "power users" are more dangerous than the rest, and we
|
||||
should avoid exposing dangerous functionality to them.
|
||||
|
||||
## Code style
|
||||
|
||||
The amphp project adheres to the PSR-2 style guide with the exception that
|
||||
opening braces for classes and methods must appear on the same line as
|
||||
the declaration:
|
||||
|
||||
https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2017 amphp
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
45
Makefile
Normal file
45
Makefile
Normal file
@ -0,0 +1,45 @@
|
||||
PHP_BIN := php
|
||||
COMPOSER_BIN := composer
|
||||
|
||||
COVERAGE = coverage
|
||||
SRCS = lib test
|
||||
|
||||
find_php_files = $(shell find $(1) -type f -name "*.php")
|
||||
src = $(foreach d,$(SRCS),$(call find_php_files,$(d)))
|
||||
|
||||
.PHONY: test
|
||||
test: setup phpunit code-style
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-coverage clean-vendor
|
||||
|
||||
.PHONY: clean-coverage
|
||||
clean-coverage:
|
||||
test ! -e coverage || rm -r coverage
|
||||
|
||||
.PHONY: clean-vendor
|
||||
clean-vendor:
|
||||
test ! -e vendor || rm -r vendor
|
||||
|
||||
.PHONY: setup
|
||||
setup: vendor/autoload.php
|
||||
|
||||
.PHONY: deps-update
|
||||
deps-update:
|
||||
$(COMPOSER_BIN) update
|
||||
|
||||
.PHONY: phpunit
|
||||
phpunit: setup
|
||||
$(PHP_BIN) vendor/bin/phpunit
|
||||
|
||||
.PHONY: code-style
|
||||
code-style: setup
|
||||
PHP_CS_FIXER_IGNORE_ENV=1 $(PHP_BIN) vendor/bin/php-cs-fixer --diff -v fix
|
||||
|
||||
composer.lock: composer.json
|
||||
$(COMPOSER_BIN) install
|
||||
touch $@
|
||||
|
||||
vendor/autoload.php: composer.lock
|
||||
$(COMPOSER_BIN) install
|
||||
touch $@
|
41
README.md
Normal file
41
README.md
Normal file
@ -0,0 +1,41 @@
|
||||
# dns
|
||||
|
||||
[![Build Status](https://img.shields.io/travis/amphp/dns/master.svg?style=flat-square)](https://travis-ci.org/amphp/dns)
|
||||
[![CoverageStatus](https://img.shields.io/coveralls/amphp/dns/master.svg?style=flat-square)](https://coveralls.io/github/amphp/dns?branch=master)
|
||||
![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)
|
||||
|
||||
`amphp/dns` provides asynchronous DNS name resolution for [Amp](https://github.com/amphp/amp).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require amphp/dns
|
||||
```
|
||||
|
||||
## Example
|
||||
|
||||
```php
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/examples/_bootstrap.php';
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Loop;
|
||||
|
||||
Loop::run(function () {
|
||||
$githubIpv4 = yield Dns\resolve("github.com", Dns\Record::A);
|
||||
pretty_print_records("github.com", $githubIpv4);
|
||||
|
||||
$googleIpv4 = Amp\Dns\resolve("google.com", Dns\Record::A);
|
||||
$googleIpv6 = Amp\Dns\resolve("google.com", Dns\Record::AAAA);
|
||||
|
||||
$firstGoogleResult = yield Amp\Promise\first([$googleIpv4, $googleIpv6]);
|
||||
pretty_print_records("google.com", $firstGoogleResult);
|
||||
|
||||
$combinedGoogleResult = yield Amp\Dns\resolve("google.com");
|
||||
pretty_print_records("google.com", $combinedGoogleResult);
|
||||
|
||||
$googleMx = yield Amp\Dns\query("google.com", Amp\Dns\Record::MX);
|
||||
pretty_print_records("google.com", $googleMx);
|
||||
});
|
||||
```
|
38
appveyor.yml
Normal file
38
appveyor.yml
Normal file
@ -0,0 +1,38 @@
|
||||
build: false
|
||||
shallow_clone: false
|
||||
|
||||
platform:
|
||||
- x86
|
||||
- x64
|
||||
|
||||
clone_folder: c:\projects\amphp
|
||||
|
||||
cache:
|
||||
- c:\tools\php73 -> appveyor.yml
|
||||
|
||||
init:
|
||||
- SET PATH=C:\Program Files\OpenSSL;c:\tools\php73;%PATH%
|
||||
- SET COMPOSER_NO_INTERACTION=1
|
||||
- SET PHP=1
|
||||
- SET ANSICON=121x90 (121x90)
|
||||
|
||||
install:
|
||||
- IF EXIST c:\tools\php73 (SET PHP=0)
|
||||
- IF %PHP%==1 sc config wuauserv start= auto
|
||||
- IF %PHP%==1 net start wuauserv
|
||||
- IF %PHP%==1 cinst -y OpenSSL.Light
|
||||
- IF %PHP%==1 cinst -y php
|
||||
- cd c:\tools\php73
|
||||
- IF %PHP%==1 copy php.ini-production php.ini /Y
|
||||
- IF %PHP%==1 echo date.timezone="UTC" >> php.ini
|
||||
- IF %PHP%==1 echo extension_dir=ext >> php.ini
|
||||
- IF %PHP%==1 echo extension=php_openssl.dll >> php.ini
|
||||
- IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini
|
||||
- IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini
|
||||
- cd c:\projects\amphp
|
||||
- appveyor DownloadFile https://getcomposer.org/composer.phar
|
||||
- php composer.phar install --prefer-dist --no-progress
|
||||
|
||||
test_script:
|
||||
- cd c:\projects\amphp
|
||||
- vendor/bin/phpunit --colors=always
|
87
composer.json
Normal file
87
composer.json
Normal file
@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "amphp/dns-over-https",
|
||||
"homepage": "https://github.com/amphp/dns-over-https",
|
||||
"description": "Async DNS-over-HTTPS resolution for Amp.",
|
||||
"keywords": [
|
||||
"dns",
|
||||
"doh",
|
||||
"dns-over-https",
|
||||
"https",
|
||||
"resolve",
|
||||
"client",
|
||||
"async",
|
||||
"amp",
|
||||
"amphp"
|
||||
],
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniil Gentili",
|
||||
"email": "daniil@daniil.it"
|
||||
},
|
||||
{
|
||||
"name": "Chris Wright",
|
||||
"email": "addr@daverandom.com"
|
||||
},
|
||||
{
|
||||
"name": "Daniel Lowrey",
|
||||
"email": "rdlowrey@php.net"
|
||||
},
|
||||
{
|
||||
"name": "Bob Weinand",
|
||||
"email": "bobwei9@hotmail.com"
|
||||
},
|
||||
{
|
||||
"name": "Niklas Keller",
|
||||
"email": "me@kelunik.com"
|
||||
},
|
||||
{
|
||||
"name": "Aaron Piotrowski",
|
||||
"email": "aaron@trowski.com"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"amphp/cache": "^1.2",
|
||||
"amphp/parser": "^1",
|
||||
"daverandom/libdns": "^2.0.1",
|
||||
"amphp/amp": "^2",
|
||||
"amphp/artax": "dev-master",
|
||||
"amphp/dns": "dev-master as v0.9.x-dev",
|
||||
"ext-filter": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"amphp/phpunit-util": "^1",
|
||||
"phpunit/phpunit": "^6",
|
||||
"amphp/php-cs-fixer-config": "dev-master"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Amp\\DoH\\": "lib"
|
||||
},
|
||||
"files": [
|
||||
"lib/functions.php"
|
||||
]
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"Amp\\DoH\\Test\\": "test"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"check": [
|
||||
"@cs",
|
||||
"@test"
|
||||
],
|
||||
"cs": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff --dry-run",
|
||||
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php-cs-fixer fix -v --diff",
|
||||
"test": "@php -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text"
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"repositories": [
|
||||
{
|
||||
"type": "git",
|
||||
"url": "https://github.com/danog/phpseclib"
|
||||
}
|
||||
]
|
||||
}
|
3
docs/.gitignore
vendored
Normal file
3
docs/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
.bundle
|
||||
_site
|
||||
Gemfile.lock
|
5
docs/Gemfile
Normal file
5
docs/Gemfile
Normal file
@ -0,0 +1,5 @@
|
||||
source "https://rubygems.org"
|
||||
gem "github-pages"
|
||||
gem "kramdown"
|
||||
gem "jekyll-github-metadata"
|
||||
gem "jekyll-relative-links"
|
26
docs/_config.yml
Normal file
26
docs/_config.yml
Normal file
@ -0,0 +1,26 @@
|
||||
kramdown:
|
||||
input: GFM
|
||||
toc_levels: 2..3
|
||||
|
||||
baseurl: "/dns"
|
||||
layouts_dir: ".shared/layout"
|
||||
includes_dir: ".shared/includes"
|
||||
|
||||
exclude: ["Gemfile", "Gemfile.lock", "README.md", "vendor"]
|
||||
safe: true
|
||||
|
||||
repository: amphp/dns
|
||||
gems:
|
||||
- "jekyll-github-metadata"
|
||||
- "jekyll-relative-links"
|
||||
|
||||
defaults:
|
||||
- scope:
|
||||
path: ""
|
||||
type: "pages"
|
||||
values:
|
||||
layout: "docs"
|
||||
description: "amphp/dns provides asynchronous DNS resolution for Amp."
|
||||
keywords: ["amphp", "amp", "dns", "name resolution", "async", "php"]
|
||||
|
||||
shared_asset_path: "/dns/asset"
|
1
docs/asset
Symbolic link
1
docs/asset
Symbolic link
@ -0,0 +1 @@
|
||||
.shared/asset
|
73
docs/index.md
Normal file
73
docs/index.md
Normal file
@ -0,0 +1,73 @@
|
||||
---
|
||||
title: Asynchronous DNS Resolution
|
||||
permalink: /
|
||||
---
|
||||
`amphp/dns` provides asynchronous DNS name resolution for [Amp](http://amphp.org/amp).
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require amphp/dns
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Configuration
|
||||
|
||||
`amphp/dns` automatically detects the system configuration and uses it. On Unix-like systems it reads `/etc/resolv.conf` and respects settings for nameservers, timeouts, and attempts. On Windows it looks up the correct entries in the Windows Registry and takes the listed nameservers. You can pass a custom `ConfigLoader` instance to `Rfc1035StubResolver` to load another configuration, such as a static config.
|
||||
|
||||
It respects the system's hosts file on Unix and Windows based systems, so it works just fine in environments like Docker with named containers.
|
||||
|
||||
The package uses a global default resolver with can be accessed and changed via `Amp\Dns\resolver()`. If an argument other than `null` is given, the given resolver is used as global instance. The instance is automatically bound to the current event loop. If you replace the event loop via `Amp\Loop::set()`, then you have to set a new global resolver.
|
||||
|
||||
Usually you don't have to change the resolver. If you want to use a custom configuration for a certain request, you can create a new resolver instance and use that instead of changing the global one.
|
||||
|
||||
### Address Resolution
|
||||
|
||||
`Amp\Dns\resolve` provides hostname to IP address resolution. It returns an array of IPv4 and IPv6 addresses by default. The type of IP addresses returned can be restricted by passing a second argument with the respective type.
|
||||
|
||||
```php
|
||||
// Example without type restriction. Will return IPv4 and / or IPv6 addresses.
|
||||
// What's returned depends on what's available for the given hostname.
|
||||
|
||||
/** @var Amp\Dns\Record[] $records */
|
||||
$records = yield Amp\Dns\resolve("github.com");
|
||||
```
|
||||
|
||||
```php
|
||||
// Example with type restriction. Will throw an exception if there are no A records.
|
||||
|
||||
/** @var Amp\Dns\Record[] $records */
|
||||
$records = yield Amp\Dns\resolve("github.com", Amp\Dns\Record::A);
|
||||
```
|
||||
|
||||
### Custom Queries
|
||||
|
||||
`Amp\Dns\query` supports the various other DNS record types such as `MX`, `PTR`, or `TXT`. It automatically rewrites passed IP addresses for `PTR` lookups.
|
||||
|
||||
```php
|
||||
/** @var Amp\Dns\Record[] $records */
|
||||
$records = Amp\Dns\query("google.com", Amp\Dns\Record::MX);
|
||||
```
|
||||
|
||||
```php
|
||||
/** @var Amp\Dns\Record[] $records */
|
||||
$records = Amp\Dns\query("8.8.8.8", Amp\Dns\Record::PTR);
|
||||
```
|
||||
|
||||
### Caching
|
||||
|
||||
The `Rfc1035StubResolver` caches responses by default in an `Amp\Cache\ArrayCache`. You can set any other `Amp\Cache\Cache` implementation by creating a custom instance of `Rfc1035StubResolver` and setting that via `Amp\Dns\resolver()`, but it's usually unnecessary. If you have a lot of very short running scripts, you might want to consider using a local DNS resolver with a cache instead of setting a custom cache implementation, such as `dnsmasq`.
|
||||
|
||||
### Reloading Configuration
|
||||
|
||||
The `Rfc1035StubResolver` (which is the default resolver shipping with that package) will cache the configuration of `/etc/resolv.conf` / the Windows Registry and the read host files by default. If you wish to reload them, you can set a periodic timer that requests a background reload of the configuration.
|
||||
|
||||
```php
|
||||
Loop::repeat(60000, function () use ($resolver) {
|
||||
yield Amp\Dns\resolver()->reloadConfig();
|
||||
});
|
||||
```
|
||||
|
||||
{:.note}
|
||||
> The above code relies on the resolver not being changed. `reloadConfig` is specific to `Rfc1035StubResolver` and is not part of the `Resolver` interface. You might want to guard the reloading with an `instanceof` check or manually set a `Rfc1035StubResolver` instance on startup to be sure it's an instance of `Rfc1035StubResolver`.
|
22
examples/_bootstrap.php
Normal file
22
examples/_bootstrap.php
Normal file
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Amp\Dns\Record;
|
||||
|
||||
require __DIR__ . "/../vendor/autoload.php";
|
||||
|
||||
function pretty_print_records(string $queryName, array $records)
|
||||
{
|
||||
print "---------- " . $queryName . " " . \str_repeat("-", 55 - \strlen($queryName)) . " TTL --\r\n";
|
||||
|
||||
$format = "%-10s %-56s %-5d\r\n";
|
||||
|
||||
foreach ($records as $record) {
|
||||
print \sprintf($format, Record::getName($record->getType()), $record->getValue(), $record->getTtl());
|
||||
}
|
||||
}
|
||||
|
||||
function pretty_print_error(string $queryName, \Throwable $error)
|
||||
{
|
||||
print "-- " . $queryName . " " . \str_repeat("-", 70 - \strlen($queryName)) . "\r\n";
|
||||
print \get_class($error) . ": " . $error->getMessage() . "\r\n";
|
||||
}
|
42
examples/benchmark.php
Normal file
42
examples/benchmark.php
Normal file
@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . "/_bootstrap.php";
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Loop;
|
||||
|
||||
print "Downloading top 500 domains..." . PHP_EOL;
|
||||
|
||||
$domains = \file_get_contents("https://moz.com/top500/domains/csv");
|
||||
$domains = \array_map(function ($line) {
|
||||
return \trim(\explode(",", $line)[1], '"/');
|
||||
}, \array_filter(\explode("\n", $domains)));
|
||||
|
||||
// Remove "URL" header
|
||||
\array_shift($domains);
|
||||
|
||||
Loop::run(function () use ($domains) {
|
||||
print "Starting sequential queries...\r\n\r\n";
|
||||
|
||||
$timings = [];
|
||||
|
||||
for ($i = 0; $i < 10; $i++) {
|
||||
$start = \microtime(1);
|
||||
$domain = $domains[\random_int(0, \count($domains) - 1)];
|
||||
|
||||
try {
|
||||
pretty_print_records($domain, yield Dns\resolve($domain));
|
||||
} catch (Dns\DnsException $e) {
|
||||
pretty_print_error($domain, $e);
|
||||
}
|
||||
|
||||
$time = \round(\microtime(1) - $start, 2);
|
||||
$timings[] = $time;
|
||||
|
||||
\printf("%'-74s\r\n\r\n", " in " . $time . " ms");
|
||||
}
|
||||
|
||||
$averageTime = \array_sum($timings) / \count($timings);
|
||||
|
||||
print "{$averageTime} ms for an average query." . PHP_EOL;
|
||||
});
|
36
examples/custom-config.php
Normal file
36
examples/custom-config.php
Normal file
@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . "/_bootstrap.php";
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\DoH;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Amp\DoH\Nameserver;
|
||||
|
||||
$customConfigLoader = new class implements Dns\ConfigLoader {
|
||||
public function loadConfig(): Promise
|
||||
{
|
||||
return Amp\call(function () {
|
||||
$hosts = yield (new Dns\HostLoader)->loadHosts();
|
||||
|
||||
return new Dns\Config([
|
||||
"8.8.8.8:53",
|
||||
"[2001:4860:4860::8888]:53",
|
||||
], $hosts, $timeout = 5000, $attempts = 3);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$DohConfig = new DoH\DoHConfig([new DoH\Nameserver('https://cloudflare-dns.com/dns-query', Nameserver::GOOGLE_JSON)]);
|
||||
Dns\resolver(new DoH\Rfc8484StubResolver($DohConfig, null, $customConfigLoader));
|
||||
|
||||
Loop::run(function () {
|
||||
$hostname = "amphp.org";
|
||||
|
||||
try {
|
||||
pretty_print_records($hostname, yield Dns\resolve($hostname));
|
||||
} catch (Dns\DnsException $e) {
|
||||
pretty_print_error($hostname, $e);
|
||||
}
|
||||
});
|
16
examples/ptr-record.php
Normal file
16
examples/ptr-record.php
Normal file
@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
require __DIR__ . "/_bootstrap.php";
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Loop;
|
||||
|
||||
Loop::run(function () {
|
||||
$ip = "8.8.8.8";
|
||||
|
||||
try {
|
||||
pretty_print_records($ip, yield Dns\query($ip, Dns\Record::PTR));
|
||||
} catch (Dns\DnsException $e) {
|
||||
pretty_print_error($ip, $e);
|
||||
}
|
||||
});
|
46
lib/DoHConfig.php
Normal file
46
lib/DoHConfig.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\DoH;
|
||||
|
||||
use Amp\Artax\Client;
|
||||
use Amp\Artax\DefaultClient;
|
||||
|
||||
final class DoHConfig
|
||||
{
|
||||
private $nameservers;
|
||||
private $artax;
|
||||
|
||||
public function __construct(array $nameservers, Client $artax = null)
|
||||
{
|
||||
if (\count($nameservers) < 1) {
|
||||
throw new ConfigException("At least one nameserver is required for a valid config");
|
||||
}
|
||||
|
||||
foreach ($nameservers as $nameserver) {
|
||||
$this->validateNameserver($nameserver);
|
||||
}
|
||||
|
||||
if ($artax === null) {
|
||||
$artax = new DefaultClient();
|
||||
}
|
||||
$this->artax = $artax;
|
||||
$this->nameservers = $nameservers;
|
||||
}
|
||||
|
||||
private function validateNameserver($nameserver)
|
||||
{
|
||||
if (!($nameserver instanceof Nameserver)) {
|
||||
throw new ConfigException("Invalid nameserver: {$nameserver}");
|
||||
}
|
||||
}
|
||||
|
||||
public function getNameservers(): array
|
||||
{
|
||||
return $this->nameservers;
|
||||
}
|
||||
|
||||
public function getArtax(): Client
|
||||
{
|
||||
return $this->artax;
|
||||
}
|
||||
}
|
284
lib/Internal/HttpsSocket.php
Normal file
284
lib/Internal/HttpsSocket.php
Normal file
@ -0,0 +1,284 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\DoH\Internal;
|
||||
|
||||
use Amp;
|
||||
use Amp\Artax\Client;
|
||||
use Amp\Artax\Request;
|
||||
use Amp\Artax\Response;
|
||||
use Amp\Deferred;
|
||||
use Amp\DoH\Nameserver;
|
||||
use Amp\Promise;
|
||||
use function Amp\call;
|
||||
use LibDNS\Decoder\DecoderFactory;
|
||||
use LibDNS\Decoder\DecodingContextFactory;
|
||||
use LibDNS\Encoder\EncoderFactory;
|
||||
use LibDNS\Messages\Message;
|
||||
use LibDNS\Messages\MessageFactory;
|
||||
use LibDNS\Packets\PacketFactory;
|
||||
use LibDNS\Records\Types\Types;
|
||||
use LibDNS\Decoder\DecodingContext;
|
||||
use LibDNS\Records\QuestionFactory;
|
||||
use LibDNS\Records\ResourceBuilderFactory;
|
||||
use LibDNS\Records\Types\TypeBuilder;
|
||||
use LibDNS\Records\Question;
|
||||
use LibDNS\Records\Resource;
|
||||
|
||||
/** @internal */
|
||||
final class HttpsSocket extends Socket
|
||||
{
|
||||
/** @var \Amp\Artax\Client */
|
||||
private $httpClient;
|
||||
|
||||
/** @var \Amp\DoH\Nameserver */
|
||||
private $nameserver;
|
||||
|
||||
/** @var \LibDNS\Encoder\Encoder */
|
||||
private $encoder;
|
||||
|
||||
/** @var \LibDNS\Decoder\Decoder */
|
||||
private $decoder;
|
||||
|
||||
/** @var \LibDNS\Messages\MessageFactory */
|
||||
private $messageFactory;
|
||||
|
||||
/** @var \LibDNS\Decoder\DecodingContextFactory */
|
||||
private $decodingContextFactory;
|
||||
|
||||
/**
|
||||
* @var \LibDNS\Packets\PacketFactory
|
||||
*/
|
||||
private $packetFactory;
|
||||
|
||||
/**
|
||||
* @var \LibDNS\Records\QuestionFactory
|
||||
*/
|
||||
private $questionFactory;
|
||||
|
||||
/**
|
||||
* @var \LibDNS\Records\ResourceBuilder
|
||||
*/
|
||||
private $resourceBuilder;
|
||||
|
||||
/**
|
||||
* @var \LibDNS\Records\Types\TypeBuilder
|
||||
*/
|
||||
private $typeBuilder;
|
||||
|
||||
/** @var \Amp\Artax\Response[] */
|
||||
private $queue;
|
||||
|
||||
/** @var \Amp\Deferred */
|
||||
private $responseDeferred;
|
||||
|
||||
public static function connect(Client $artax, Nameserver $nameserver): self
|
||||
{
|
||||
return new self($artax, $nameserver);
|
||||
}
|
||||
|
||||
protected function __construct(Client $artax, Nameserver $nameserver)
|
||||
{
|
||||
$this->httpClient = $artax;
|
||||
$this->nameserver = $nameserver;
|
||||
|
||||
$this->queue = new \SplQueue;
|
||||
|
||||
if ($nameserver->getType() !== Nameserver::GOOGLE_JSON) {
|
||||
$this->encoder = (new EncoderFactory)->create();
|
||||
$this->decoder = (new DecoderFactory)->create();
|
||||
} else {
|
||||
$this->messageFactory = new MessageFactory;
|
||||
$this->decodingContextFactory = new DecodingContextFactory;
|
||||
$this->packetFactory = new PacketFactory;
|
||||
$this->questionFactory = new QuestionFactory;
|
||||
$this->resourceBuilder = (new ResourceBuilderFactory)->create();
|
||||
$this->typeBuilder = new TypeBuilder;
|
||||
}
|
||||
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
protected function send(Message $message): Promise
|
||||
{
|
||||
$id = $message->getID();
|
||||
$data = $this->encoder->encode($message);
|
||||
|
||||
switch ($this->nameserver->getType()) {
|
||||
case Nameserver::RFC8484_GET:
|
||||
$request = (new Request($this->nameserver->getUri().'?'.http_build_query(['dns' => $data]), "GET"))
|
||||
->withHeader('accept', 'application/dns-message')
|
||||
->withHeaders($this->nameserver->getHeaders());
|
||||
break;
|
||||
case Nameserver::RFC8484_POST:
|
||||
$request = (new Request($this->nameserver->getUri(), "POST"))
|
||||
->withBody($data)
|
||||
->withHeader('content-type', 'application/dns-message')
|
||||
->withHeader('accept', 'application/dns-message')
|
||||
->withHeaders($this->nameserver->getHeaders());
|
||||
break;
|
||||
case Nameserver::GOOGLE_JSON:
|
||||
$param = [
|
||||
'cd' => 0, // Do not disable result validation
|
||||
'do' => 0, // Do not send me DNSSEC data
|
||||
'type' => $message->getType(), // Record type being requested
|
||||
'name' => $message->getQuestionRecords()->getRecordByIndex(0),
|
||||
];
|
||||
$request = (new Request($this->nameserver->getUri().'?'.http_build_query($param), "GET"))
|
||||
->withHeaders($this->nameserver->getHeaders());
|
||||
break;
|
||||
}
|
||||
|
||||
$deferred = new Deferred;
|
||||
$promise = $this->httpClient->request($request);
|
||||
$promise->onResolve(function (\Throwable $error = null, Response $result = null) use ($deferred, $id) {
|
||||
if ($error) {
|
||||
$deferred->fail($error);
|
||||
return;
|
||||
}
|
||||
if ($result) {
|
||||
$this->queueResponse($result, $id);
|
||||
$deferred->resolve();
|
||||
}
|
||||
});
|
||||
|
||||
return $deferred->promise();
|
||||
}
|
||||
public function queueResponse(Response $result, int $id)
|
||||
{
|
||||
$this->queue->push([$result, $id]);
|
||||
if ($this->responseDeferred) {
|
||||
$this->responseDeferred->resolve();
|
||||
//Loop::defer([$this->responseDeferred, 'resolve']);
|
||||
}
|
||||
}
|
||||
protected function receive(): Promise
|
||||
{
|
||||
return call(function () {
|
||||
/** @var $result \Amp\Artax\Response */
|
||||
if ($this->queue->isEmpty()) {
|
||||
if (!$this->responseDeferred) {
|
||||
$this->responseDeferred = new Deferred;
|
||||
}
|
||||
yield $this->responseDeferred;
|
||||
$this->responseDeferred = new Deferred;
|
||||
|
||||
list($result, $requestId) = $this->queue->shift();
|
||||
}
|
||||
|
||||
list($result, $requestId) = $this->queue->shift();
|
||||
|
||||
switch ($this->nameserver->getType()) {
|
||||
case Nameserver::RFC8484_GET:
|
||||
case Nameserver::RFC8484_POST:
|
||||
return $this->decoder->decode(yield $result->getBody());
|
||||
case Nameserver::GOOGLE_JSON:
|
||||
$result = json_decode($result, true);
|
||||
|
||||
$message = $this->messageFactory->create();
|
||||
$decodingContext = $this->decodingContextFactory->create($this->packetFactory->create());
|
||||
|
||||
//$message->isAuthoritative(true);
|
||||
$message->setID($requestId);
|
||||
$message->setResponseCode($result['Status']);
|
||||
$message->isTruncated($result['TC']);
|
||||
$message->isRecursionDesired($result['RD']);
|
||||
$message->isRecursionAvailable($result['RA']);
|
||||
|
||||
$decodingContext->setExpectedQuestionRecords(isset($result['Question']) ? count($result['Question']) : 0);
|
||||
$decodingContext->setExpectedAnswerRecords(isset($result['Answer']) ? count($result['Answer']) : 0);
|
||||
$decodingContext->setExpectedAuthorityRecords(0);
|
||||
$decodingContext->setExpectedAdditionalRecords(isset($result['Additional']) ? count($result['Additional']) : 0);
|
||||
|
||||
$questionRecords = $message->getQuestionRecords();
|
||||
$expected = $decodingContext->getExpectedQuestionRecords();
|
||||
for ($i = 0; $i < $expected; $i++) {
|
||||
$questionRecords->add($this->decodeQuestionRecord($decodingContext, $result['Question'][$i]));
|
||||
}
|
||||
|
||||
$answerRecords = $message->getAnswerRecords();
|
||||
$expected = $decodingContext->getExpectedAnswerRecords();
|
||||
for ($i = 0; $i < $expected; $i++) {
|
||||
$answerRecords->add($this->decodeResourceRecord($decodingContext, $result['Answer'][$i]));
|
||||
}
|
||||
|
||||
$authorityRecords = $message->getAuthorityRecords();
|
||||
$expected = $decodingContext->getExpectedAuthorityRecords();
|
||||
for ($i = 0; $i < $expected; $i++) {
|
||||
$authorityRecords->add($this->decodeResourceRecord($decodingContext, $result['Authority'][$i]));
|
||||
}
|
||||
|
||||
$additionalRecords = $message->getAdditionalRecords();
|
||||
$expected = $decodingContext->getExpectedAdditionalRecords();
|
||||
for ($i = 0; $i < $expected; $i++) {
|
||||
$additionalRecords->add($this->decodeResourceRecord($decodingContext, $result['Additional'][$i]));
|
||||
}
|
||||
|
||||
return $message;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Decode a question record
|
||||
*
|
||||
* @param \LibDNS\Decoder\DecodingContext $decodingContext
|
||||
* @return \LibDNS\Records\Question
|
||||
* @throws \UnexpectedValueException When the record is invalid
|
||||
*/
|
||||
private function decodeQuestionRecord(DecodingContext $decodingContext, array $record): Question
|
||||
{
|
||||
/** @var \LibDNS\Records\Types\DomainName $domainName */
|
||||
$domainName = $this->typeBuilder->build(Types::DOMAIN_NAME);
|
||||
$domainName->setLabels(explode('.', $record['name']));
|
||||
|
||||
$question = $this->questionFactory->create($record['type']);
|
||||
$question->setName($domainName);
|
||||
//$question->setClass($meta['class']);
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decode a resource record
|
||||
*
|
||||
* @param \LibDNS\Decoder\DecodingContext $decodingContext
|
||||
* @return \LibDNS\Records\Resource
|
||||
* @throws \UnexpectedValueException When the record is invalid
|
||||
* @throws \InvalidArgumentException When a type subtype is unknown
|
||||
*/
|
||||
private function decodeResourceRecord(DecodingContext $decodingContext, array $record): Resource
|
||||
{
|
||||
/** @var \LibDNS\Records\Types\DomainName $domainName */
|
||||
$domainName = $this->typeBuilder->build(Types::DOMAIN_NAME);
|
||||
$domainName->setLabels(explode('.', $record['name']));
|
||||
|
||||
$resource = $this->resourceBuilder->build($record['type']);
|
||||
$resource->setName($domainName);
|
||||
//$resource->setClass($meta['class']);
|
||||
$resource->setTTL($record['ttl']);
|
||||
|
||||
$data = $resource->getData();
|
||||
|
||||
$fieldDef = $index = null;
|
||||
foreach ($resource->getData()->getTypeDefinition() as $index => $fieldDef) {
|
||||
$field = $this->typeBuilder->build($fieldDef->getType());
|
||||
$remainingLength -= $this->decodeType($decodingContext, $field, $remainingLength);
|
||||
$data->setField($index, $field);
|
||||
}
|
||||
|
||||
if ($fieldDef->allowsMultiple()) {
|
||||
while ($remainingLength) {
|
||||
$field = $this->typeBuilder->build($fieldDef->getType());
|
||||
$remainingLength -= $this->decodeType($decodingContext, $field, $remainingLength);
|
||||
$data->setField(++$index, $field);
|
||||
}
|
||||
}
|
||||
|
||||
if ($remainingLength !== 0) {
|
||||
throw new \UnexpectedValueException('Decode error: Invalid length for record data section');
|
||||
}
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
233
lib/Internal/Socket.php
Normal file
233
lib/Internal/Socket.php
Normal file
@ -0,0 +1,233 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\DoH\Internal;
|
||||
|
||||
use Amp;
|
||||
use Amp\ByteStream\ResourceInputStream;
|
||||
use Amp\ByteStream\ResourceOutputStream;
|
||||
use Amp\ByteStream\StreamException;
|
||||
use Amp\Deferred;
|
||||
use Amp\Dns\DnsException;
|
||||
use Amp\Dns\TimeoutException;
|
||||
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 bool */
|
||||
private $receiving = false;
|
||||
|
||||
/** @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(Client $artax, Nameserver $nameserver): Socket;
|
||||
|
||||
/**
|
||||
* @param Message $message
|
||||
*
|
||||
* @return Promise<int>
|
||||
*/
|
||||
abstract protected function send(Message $message): Promise;
|
||||
|
||||
/**
|
||||
* @return Promise<Message>
|
||||
*/
|
||||
abstract protected function receive(): Promise;
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
abstract public function close();
|
||||
|
||||
protected function __construct()
|
||||
{
|
||||
$this->messageFactory = new MessageFactory;
|
||||
|
||||
$this->onResolve = function (\Throwable $exception = null, Message $message = null) {
|
||||
$this->receiving = false;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
if (empty($this->pending)) {
|
||||
$this->input->unreference();
|
||||
} elseif (!$this->receiving) {
|
||||
$this->input->reference();
|
||||
$this->receiving = true;
|
||||
$this->receive()->onResolve($this->onResolve);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @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 {
|
||||
yield $this->send($message);
|
||||
} catch (StreamException $exception) {
|
||||
$exception = new DnsException("Sending the request failed", 0, $exception);
|
||||
$this->error($exception);
|
||||
throw $exception;
|
||||
}
|
||||
|
||||
$this->input->reference();
|
||||
|
||||
if (!$this->receiving) {
|
||||
$this->receiving = true;
|
||||
$this->receive()->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]);
|
||||
|
||||
if (empty($this->pending)) {
|
||||
$this->input->unreference();
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
$this->close();
|
||||
|
||||
if (empty($this->pending)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$exception instanceof DnsException) {
|
||||
$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;
|
||||
}
|
||||
}
|
44
lib/Nameserver.php
Normal file
44
lib/Nameserver.php
Normal file
@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\DoH;
|
||||
|
||||
final class Nameserver
|
||||
{
|
||||
const RFC8484_GET = 0;
|
||||
const RFC8484_POST = 1;
|
||||
const GOOGLE_JSON = 2;
|
||||
|
||||
private $type;
|
||||
private $uri;
|
||||
private $headers = [];
|
||||
|
||||
public function __construct(string $uri, int $type = self::RFC8484_POST, array $headers = [])
|
||||
{
|
||||
$this->uri = $uri;
|
||||
$this->type = $type;
|
||||
$this->headers = $headers;
|
||||
}
|
||||
public function getUri(): string
|
||||
{
|
||||
return $this->uri;
|
||||
}
|
||||
public function getHeaders(): array
|
||||
{
|
||||
return $this->headers;
|
||||
}
|
||||
public function getType(): int
|
||||
{
|
||||
return $this->type;
|
||||
}
|
||||
public function __toString(): string
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
||||
}
|
379
lib/Rfc8484StubResolver.php
Normal file
379
lib/Rfc8484StubResolver.php
Normal file
@ -0,0 +1,379 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\DoH;
|
||||
|
||||
use Amp\Cache\ArrayCache;
|
||||
use Amp\Cache\Cache;
|
||||
use Amp\Dns\DnsException;
|
||||
use Amp\Dns\NoRecordException;
|
||||
use Amp\DoH\Internal\Socket;
|
||||
use Amp\DoH\Internal\TcpSocket;
|
||||
use Amp\DoH\Internal\UdpSocket;
|
||||
use Amp\Loop;
|
||||
use Amp\MultiReasonException;
|
||||
use Amp\Promise;
|
||||
use Amp\Success;
|
||||
use function Amp\call;
|
||||
use LibDNS\Messages\Message;
|
||||
use LibDNS\Records\Question;
|
||||
use LibDNS\Records\QuestionFactory;
|
||||
use Amp\Dns\TimeoutException;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Dns\Resolver;
|
||||
use Amp\DoH\Internal\HttpsSocket;
|
||||
use Amp\Dns\ConfigLoader;
|
||||
|
||||
final class Rfc8484StubResolver implements Resolver
|
||||
{
|
||||
const CACHE_PREFIX = "amphp.doh.";
|
||||
|
||||
/** @var \Amp\Dns\ConfigLoader */
|
||||
private $configLoader;
|
||||
|
||||
/** @var \LibDNS\Records\QuestionFactory */
|
||||
private $questionFactory;
|
||||
|
||||
/** @var \Amp\Dns\Config|null */
|
||||
private $config;
|
||||
|
||||
/** @var Promise|null */
|
||||
private $pendingConfig;
|
||||
|
||||
/** @var \Amp\DoH\DoHConfig */
|
||||
private $dohConfig;
|
||||
|
||||
/** @var Cache */
|
||||
private $cache;
|
||||
|
||||
/** @var Promise[] */
|
||||
private $pendingQueries = [];
|
||||
|
||||
public function __construct(DoHConfig $config, Cache $cache = null, ConfigLoader $configLoader = null)
|
||||
{
|
||||
$this->cache = $cache ?? new ArrayCache(5000/* default gc interval */, 256/* size */);
|
||||
$this->configLoader = $configLoader ?? (\stripos(PHP_OS, "win") === 0
|
||||
? new WindowsConfigLoader()
|
||||
: new UnixConfigLoader);
|
||||
$this->dohConfig = $config;
|
||||
|
||||
$this->questionFactory = new QuestionFactory;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function resolve(string $name, int $typeRestriction = null): Promise
|
||||
{
|
||||
if ($typeRestriction !== null && $typeRestriction !== Record::A && $typeRestriction !== Record::AAAA) {
|
||||
throw new \Error("Invalid value for parameter 2: null|Record::A|Record::AAAA expected");
|
||||
}
|
||||
|
||||
return call(function () use ($name, $typeRestriction) {
|
||||
if (!$this->config) {
|
||||
yield $this->reloadConfig();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
$name = normalizeName($name);
|
||||
|
||||
if ($records = $this->queryHosts($name, $typeRestriction)) {
|
||||
return $records;
|
||||
}
|
||||
|
||||
// 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('127.0.0.1', Record::A, null)];
|
||||
}
|
||||
|
||||
for ($redirects = 0; $redirects < 5; $redirects++) {
|
||||
try {
|
||||
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);
|
||||
|
||||
break; // Break redirect loop, otherwise we query the same records 5 times
|
||||
} catch (MultiReasonException $e) {
|
||||
$errors = [];
|
||||
|
||||
foreach ($e->getReasons() as $reason) {
|
||||
if ($reason instanceof NoRecordException) {
|
||||
throw $reason;
|
||||
}
|
||||
|
||||
$errors[] = $reason->getMessage();
|
||||
}
|
||||
|
||||
throw new DnsException("All query attempts failed for {$name}: ".\implode(", ", $errors), 0, $e);
|
||||
}
|
||||
}
|
||||
} catch (NoRecordException $e) {
|
||||
try {
|
||||
/** @var Record[] $cnameRecords */
|
||||
$cnameRecords = yield $this->query($name, Record::CNAME);
|
||||
$name = $cnameRecords[0]->getValue();
|
||||
continue;
|
||||
} catch (NoRecordException $e) {
|
||||
/** @var Record[] $dnameRecords */
|
||||
$dnameRecords = yield $this->query($name, Record::DNAME);
|
||||
$name = $dnameRecords[0]->getValue();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $records;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the configuration in the background.
|
||||
*
|
||||
* Once it's finished, the configuration will be used for new requests.
|
||||
*
|
||||
* @return Promise
|
||||
*/
|
||||
public function reloadConfig(): Promise
|
||||
{
|
||||
if ($this->pendingConfig) {
|
||||
return $this->pendingConfig;
|
||||
}
|
||||
|
||||
$promise = call(function () {
|
||||
$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
|
||||
{
|
||||
$hosts = $this->config->getKnownHosts();
|
||||
$records = [];
|
||||
|
||||
$returnIPv4 = $typeRestriction === null || $typeRestriction === Record::A;
|
||||
$returnIPv6 = $typeRestriction === null || $typeRestriction === Record::AAAA;
|
||||
|
||||
if ($returnIPv4 && isset($hosts[Record::A][$name])) {
|
||||
$records[] = new Record($hosts[Record::A][$name], Record::A, null);
|
||||
}
|
||||
|
||||
if ($returnIPv6 && isset($hosts[Record::AAAA][$name])) {
|
||||
$records[] = new Record($hosts[Record::AAAA][$name], Record::AAAA, null);
|
||||
}
|
||||
|
||||
return $records;
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
public function query(string $name, int $type): Promise
|
||||
{
|
||||
$pendingQueryKey = $type." ".$name;
|
||||
|
||||
if (isset($this->pendingQueries[$pendingQueryKey])) {
|
||||
return $this->pendingQueries[$pendingQueryKey];
|
||||
}
|
||||
|
||||
$promise = call(function () use ($name, $type) {
|
||||
if (!$this->config) {
|
||||
yield $this->reloadConfig();
|
||||
}
|
||||
|
||||
$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();
|
||||
$attempt = 0;
|
||||
|
||||
/** @var Socket $socket */
|
||||
$nameserver = $nameservers[0];
|
||||
$socket = $this->getSocket($nameserver);
|
||||
|
||||
$attemptDescription = [];
|
||||
|
||||
while ($attempt < $attempts) {
|
||||
try {
|
||||
$attemptDescription[] = $nameserver;
|
||||
|
||||
/** @var Message $response */
|
||||
$response = yield $socket->ask($question, $this->config->getTimeout());
|
||||
$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 = yield $this->getSocket($nameserver);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TimeoutException(\sprintf(
|
||||
"No response for '%s' (%s) from any nameserver after %d attempts, tried %s",
|
||||
$name,
|
||||
Record::getName($type),
|
||||
$attempts,
|
||||
\implode(", ", $attemptDescription)
|
||||
));
|
||||
});
|
||||
|
||||
$this->pendingQueries[$type." ".$name] = $promise;
|
||||
$promise->onResolve(function () use ($name, $type) {
|
||||
unset($this->pendingQueries[$type." ".$name]);
|
||||
});
|
||||
|
||||
return $promise;
|
||||
}
|
||||
|
||||
private function normalizeName(string $name, int $type)
|
||||
{
|
||||
if ($type === Record::PTR) {
|
||||
if (($packedIp = @\inet_pton($name)) !== false) {
|
||||
if (isset($packedIp[4])) { // IPv6
|
||||
$name = \wordwrap(\strrev(\bin2hex($packedIp)), 1, ".", true).".ip6.arpa";
|
||||
} else { // IPv4
|
||||
$name = \inet_ntop(\strrev($packedIp)).".in-addr.arpa";
|
||||
}
|
||||
}
|
||||
} elseif (\in_array($type, [Record::A, Record::AAAA])) {
|
||||
$name = normalizeName($name);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @param int $type
|
||||
*
|
||||
* @return \LibDNS\Records\Question
|
||||
*/
|
||||
private function createQuestion(string $name, int $type): Question
|
||||
{
|
||||
if (0 > $type || 0xffff < $type) {
|
||||
$message = \sprintf('%d does not correspond to a valid record type (must be between 0 and 65535).', $type);
|
||||
throw new \Error($message);
|
||||
}
|
||||
|
||||
$question = $this->questionFactory->create($type);
|
||||
$question->setName($name);
|
||||
|
||||
return $question;
|
||||
}
|
||||
|
||||
private function getCacheKey(string $name, int $type): string
|
||||
{
|
||||
return self::CACHE_PREFIX.$name."#".$type;
|
||||
}
|
||||
|
||||
private function decodeCachedResult(string $name, int $type, string $encoded)
|
||||
{
|
||||
$decoded = \json_decode($encoded, true);
|
||||
|
||||
if (!$decoded) {
|
||||
throw new NoRecordException("No records returned for {$name} (cached result)");
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($decoded as $data) {
|
||||
$result[] = new Record($data, $type);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function getSocket(Nameserver $nameserver)
|
||||
{
|
||||
$uri = $nameserver->getUri();
|
||||
if (isset($this->sockets[$uri])) {
|
||||
return new Success($this->sockets[$uri]);
|
||||
}
|
||||
|
||||
|
||||
$this->sockets[$uri] = HttpsSocket::connect($this->dohConfig->getArtax(), $nameserver);
|
||||
|
||||
return $this->sockets[$uri];
|
||||
}
|
||||
|
||||
private function assertAcceptableResponse(Message $response)
|
||||
{
|
||||
if ($response->getResponseCode() !== 0) {
|
||||
throw new DnsException(\sprintf("Server returned error code: %d", $response->getResponseCode()));
|
||||
}
|
||||
}
|
||||
}
|
109
lib/functions.php
Normal file
109
lib/functions.php
Normal file
@ -0,0 +1,109 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\DoH;
|
||||
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use Amp\Dns\Resolver;
|
||||
use Amp\Dns\InvalidNameException;
|
||||
|
||||
const LOOP_STATE_IDENTIFIER = Resolver::class;
|
||||
|
||||
/**
|
||||
* Retrieve the application-wide dns resolver instance.
|
||||
*
|
||||
* @param \Amp\DoH\Resolver $resolver Optionally specify a new default dns resolver instance
|
||||
*
|
||||
* @return \Amp\DoH\Resolver Returns the application-wide dns resolver instance
|
||||
*/
|
||||
function resolver(Resolver $resolver = null): Resolver
|
||||
{
|
||||
if ($resolver === null) {
|
||||
$resolver = Loop::getState(LOOP_STATE_IDENTIFIER);
|
||||
|
||||
if ($resolver) {
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
$resolver = createDefaultResolver();
|
||||
}
|
||||
|
||||
Loop::setState(LOOP_STATE_IDENTIFIER, $resolver);
|
||||
|
||||
return $resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new dns resolver best-suited for the current environment.
|
||||
*
|
||||
* @return \Amp\DoH\Resolver
|
||||
*/
|
||||
function createDefaultResolver(): Resolver
|
||||
{
|
||||
return new Rfc1035StubResolver();
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Resolver::resolve()
|
||||
*/
|
||||
function resolve(string $name, int $typeRestriction = null): Promise
|
||||
{
|
||||
return resolver()->resolve($name, $typeRestriction);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see Resolver::query()
|
||||
*/
|
||||
function query(string $name, int $type): Promise
|
||||
{
|
||||
return resolver()->query($name, $type);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a string is a valid DNS name.
|
||||
*
|
||||
* @param string $name String to check.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function isValidName(string $name)
|
||||
{
|
||||
try {
|
||||
normalizeName($name);
|
||||
return true;
|
||||
} catch (InvalidNameException $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a DNS name and automatically checks it for validity.
|
||||
*
|
||||
* @param string $name DNS name.
|
||||
*
|
||||
* @return string Normalized DNS name.
|
||||
* @throws InvalidNameException If an invalid name or an IDN name without ext/intl being installed has been passed.
|
||||
*/
|
||||
function normalizeName(string $name): string
|
||||
{
|
||||
static $pattern = '/^(?<name>[a-z0-9]([a-z0-9-_]{0,61}[a-z0-9])?)(\.(?&name))*$/i';
|
||||
|
||||
if (\function_exists('idn_to_ascii') && \defined('INTL_IDNA_VARIANT_UTS46')) {
|
||||
if (false === $result = \idn_to_ascii($name, 0, \INTL_IDNA_VARIANT_UTS46)) {
|
||||
throw new InvalidNameException("Name '{$name}' could not be processed for IDN.");
|
||||
}
|
||||
|
||||
$name = $result;
|
||||
} elseif (\preg_match('/[\x80-\xff]/', $name)) {
|
||||
throw new InvalidNameException(
|
||||
"Name '{$name}' contains non-ASCII characters and IDN support is not available. " .
|
||||
"Verify that ext/intl is installed for IDN support and that ICU is at least version 4.6."
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($name[253]) || !\preg_match($pattern, $name)) {
|
||||
throw new InvalidNameException("Name '{$name}' is not a valid hostname.");
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
28
phpunit.xml.dist
Normal file
28
phpunit.xml.dist
Normal file
@ -0,0 +1,28 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/6.0/phpunit.xsd"
|
||||
backupGlobals="false"
|
||||
backupStaticAttributes="false"
|
||||
bootstrap="vendor/autoload.php"
|
||||
colors="true"
|
||||
convertErrorsToExceptions="true"
|
||||
convertNoticesToExceptions="true"
|
||||
convertWarningsToExceptions="true"
|
||||
processIsolation="false"
|
||||
stopOnFailure="false"
|
||||
>
|
||||
<testsuites>
|
||||
<testsuite name="Main">
|
||||
<directory>test</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<filter>
|
||||
<whitelist>
|
||||
<directory suffix=".php">lib</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<listeners>
|
||||
<listener class="Amp\PHPUnit\LoopReset"/>
|
||||
</listeners>
|
||||
</phpunit>
|
77
test/ConfigTest.php
Normal file
77
test/ConfigTest.php
Normal file
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns\Config;
|
||||
use Amp\Dns\ConfigException;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
|
||||
class ConfigTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param string[] $nameservers Valid server array.
|
||||
*
|
||||
* @dataProvider provideValidServers
|
||||
*/
|
||||
public function testAcceptsValidServers(array $nameservers)
|
||||
{
|
||||
$this->assertInstanceOf(Config::class, new Config($nameservers));
|
||||
}
|
||||
|
||||
public function provideValidServers()
|
||||
{
|
||||
return [
|
||||
[["127.1.1.1"]],
|
||||
[["127.1.1.1:1"]],
|
||||
[["[::1]:52"]],
|
||||
[["[::1]"]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $nameservers Invalid server array.
|
||||
*
|
||||
* @dataProvider provideInvalidServers
|
||||
*/
|
||||
public function testRejectsInvalidServers(array $nameservers)
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
new Config($nameservers);
|
||||
}
|
||||
|
||||
public function provideInvalidServers()
|
||||
{
|
||||
return [
|
||||
[[]],
|
||||
[[42]],
|
||||
[[null]],
|
||||
[[true]],
|
||||
[["foobar"]],
|
||||
[["foobar.com"]],
|
||||
[["127.1.1"]],
|
||||
[["127.1.1.1.1"]],
|
||||
[["126.0.0.5", "foobar"]],
|
||||
[["42"]],
|
||||
[["::1"]],
|
||||
[["::1:53"]],
|
||||
[["[::1]:"]],
|
||||
[["[::1]:76235"]],
|
||||
[["[::1]:0"]],
|
||||
[["[::1]:-1"]],
|
||||
[["[::1:51"]],
|
||||
[["[::1]:abc"]],
|
||||
];
|
||||
}
|
||||
|
||||
public function testInvalidTimeout()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
new Config(["127.0.0.1"], [], -1);
|
||||
}
|
||||
|
||||
public function testInvalidAttempts()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
new Config(["127.0.0.1"], [], 500, 0);
|
||||
}
|
||||
}
|
23
test/DecodeTest.php
Normal file
23
test/DecodeTest.php
Normal file
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\PHPUnit\TestCase;
|
||||
use LibDNS\Decoder\DecoderFactory;
|
||||
use LibDNS\Messages\Message;
|
||||
|
||||
class DecodeTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Regression test for https://github.com/amphp/dns/issues/53 and other reported issues.
|
||||
*/
|
||||
public function testDecodesEmptyDomains()
|
||||
{
|
||||
$message = \hex2bin("37ed818000010005000d000005676d61696c03636f6d00000f0001c00c000f000100000dff0020000a04616c74310d676d61696c2d736d74702d696e016c06676f6f676c65c012c00c000f000100000dff0009001404616c7432c02ec00c000f000100000dff0009002804616c7434c02ec00c000f000100000dff0009001e04616c7433c02ec00c000f000100000dff00040005c02e0000020001000026b50014016c0c726f6f742d73657276657273036e6574000000020001000026b500040163c0a30000020001000026b500040164c0a30000020001000026b50004016ac0a30000020001000026b500040162c0a30000020001000026b500040161c0a30000020001000026b500040167c0a30000020001000026b50004016bc0a30000020001000026b500040165c0a30000020001000026b50004016dc0a30000020001000026b500040169c0a30000020001000026b500040166c0a30000020001000026b500040168c0a3");
|
||||
|
||||
$decoder = (new DecoderFactory)->create();
|
||||
$response = $decoder->decode($message);
|
||||
|
||||
$this->assertInstanceOf(Message::class, $response);
|
||||
}
|
||||
}
|
43
test/HostLoaderTest.php
Normal file
43
test/HostLoaderTest.php
Normal file
@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns\HostLoader;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Loop;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
|
||||
class HostLoaderTest extends TestCase
|
||||
{
|
||||
public function testIgnoresCommentsAndParsesBasicEntry()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$loader = new HostLoader(__DIR__ . "/data/hosts");
|
||||
$this->assertSame([
|
||||
Record::A => [
|
||||
"localhost" => "127.0.0.1",
|
||||
],
|
||||
], yield $loader->loadHosts());
|
||||
});
|
||||
}
|
||||
|
||||
public function testReturnsEmptyErrorOnFileNotFound()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$loader = new HostLoader(__DIR__ . "/data/hosts.not.found");
|
||||
$this->assertSame([], yield $loader->loadHosts());
|
||||
});
|
||||
}
|
||||
|
||||
public function testIgnoresInvalidNames()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$loader = new HostLoader(__DIR__ . "/data/hosts.invalid.name");
|
||||
$this->assertSame([
|
||||
Record::A => [
|
||||
"localhost" => "127.0.0.1",
|
||||
],
|
||||
], yield $loader->loadHosts());
|
||||
});
|
||||
}
|
||||
}
|
126
test/IntegrationTest.php
Normal file
126
test/IntegrationTest.php
Normal file
@ -0,0 +1,126 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Loop;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
|
||||
class IntegrationTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @param string $hostname
|
||||
* @group internet
|
||||
* @dataProvider provideHostnames
|
||||
*/
|
||||
public function testResolve($hostname)
|
||||
{
|
||||
Loop::run(function () use ($hostname) {
|
||||
$result = yield Dns\resolve($hostname);
|
||||
|
||||
/** @var Record $record */
|
||||
$record = $result[0];
|
||||
$inAddr = @\inet_pton($record->getValue());
|
||||
$this->assertNotFalse(
|
||||
$inAddr,
|
||||
"Server name $hostname did not resolve to a valid IP address"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @group internet
|
||||
*/
|
||||
public function testWorksAfterConfigReload()
|
||||
{
|
||||
Loop::run(function () {
|
||||
yield Dns\query("google.com", Record::A);
|
||||
$this->assertNull(yield Dns\resolver()->reloadConfig());
|
||||
$this->assertInternalType("array", yield Dns\query("example.com", Record::A));
|
||||
});
|
||||
}
|
||||
|
||||
public function testResolveIPv4only()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$records = yield Dns\resolve("google.com", Record::A);
|
||||
|
||||
/** @var Record $record */
|
||||
foreach ($records as $record) {
|
||||
$this->assertSame(Record::A, $record->getType());
|
||||
$inAddr = @\inet_pton($record->getValue());
|
||||
$this->assertNotFalse(
|
||||
$inAddr,
|
||||
"Server name google.com did not resolve to a valid IP address"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function testResolveIPv6only()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$records = yield Dns\resolve("google.com", Record::AAAA);
|
||||
|
||||
/** @var Record $record */
|
||||
foreach ($records as $record) {
|
||||
$this->assertSame(Record::AAAA, $record->getType());
|
||||
$inAddr = @\inet_pton($record->getValue());
|
||||
$this->assertNotFalse(
|
||||
$inAddr,
|
||||
"Server name google.com did not resolve to a valid IP address"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function testPtrLookup()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$result = yield Dns\query("8.8.4.4", Record::PTR);
|
||||
|
||||
/** @var Record $record */
|
||||
$record = $result[0];
|
||||
$this->assertSame("google-public-dns-b.google.com", $record->getValue());
|
||||
$this->assertNotNull($record->getTtl());
|
||||
$this->assertSame(Record::PTR, $record->getType());
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Test that two concurrent requests to the same resource share the same request and do not result in two requests
|
||||
* being sent.
|
||||
*/
|
||||
public function testRequestSharing()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$promise1 = Dns\query("example.com", Record::A);
|
||||
$promise2 = Dns\query("example.com", Record::A);
|
||||
|
||||
$this->assertSame($promise1, $promise2);
|
||||
$this->assertSame(yield $promise1, yield $promise2);
|
||||
});
|
||||
}
|
||||
|
||||
public function provideHostnames()
|
||||
{
|
||||
return [
|
||||
["google.com"],
|
||||
["github.com"],
|
||||
["stackoverflow.com"],
|
||||
["blog.kelunik.com"], /* that's a CNAME to GH pages */
|
||||
["localhost"],
|
||||
["192.168.0.1"],
|
||||
["::1"],
|
||||
];
|
||||
}
|
||||
|
||||
public function provideServers()
|
||||
{
|
||||
return [
|
||||
["8.8.8.8"],
|
||||
["8.8.8.8:53"],
|
||||
];
|
||||
}
|
||||
}
|
27
test/RecordTest.php
Normal file
27
test/RecordTest.php
Normal file
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns\Record;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
|
||||
class RecordTest extends TestCase
|
||||
{
|
||||
public function testGetName()
|
||||
{
|
||||
$this->assertSame("A", Record::getName(Record::A));
|
||||
}
|
||||
|
||||
public function testGetNameOnInvalidRecordType()
|
||||
{
|
||||
$this->expectException(\Error::class);
|
||||
$this->expectExceptionMessage("65536 does not correspond to a valid record type (must be between 0 and 65535).");
|
||||
|
||||
Record::getName(65536);
|
||||
}
|
||||
|
||||
public function testGetNameOnUnknownRecordType()
|
||||
{
|
||||
$this->assertSame("unknown (1000)", Record::getName(1000));
|
||||
}
|
||||
}
|
45
test/Rfc1035StubResolverTest.php
Normal file
45
test/Rfc1035StubResolverTest.php
Normal file
@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns\DnsException;
|
||||
use Amp\Dns\InvalidNameException;
|
||||
use Amp\Dns\Record;
|
||||
use Amp\Dns\Rfc1035StubResolver;
|
||||
use Amp\Loop;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
|
||||
class Rfc1035StubResolverTest extends TestCase
|
||||
{
|
||||
public function testResolveSecondParameterAcceptedValues()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$this->expectException(\Error::class);
|
||||
(new Rfc1035StubResolver)->resolve("abc.de", Record::TXT);
|
||||
});
|
||||
}
|
||||
|
||||
public function testIpAsArgumentWithIPv4Restriction()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$this->expectException(DnsException::class);
|
||||
yield (new Rfc1035StubResolver)->resolve("::1", Record::A);
|
||||
});
|
||||
}
|
||||
|
||||
public function testIpAsArgumentWithIPv6Restriction()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$this->expectException(DnsException::class);
|
||||
yield (new Rfc1035StubResolver)->resolve("127.0.0.1", Record::AAAA);
|
||||
});
|
||||
}
|
||||
|
||||
public function testInvalidName()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$this->expectException(InvalidNameException::class);
|
||||
yield (new Rfc1035StubResolver)->resolve("go@gle.com", Record::A);
|
||||
});
|
||||
}
|
||||
}
|
46
test/SocketTest.php
Normal file
46
test/SocketTest.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Loop;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
use Amp\Promise;
|
||||
use LibDNS\Messages\Message;
|
||||
use LibDNS\Messages\MessageTypes;
|
||||
use LibDNS\Records\QuestionFactory;
|
||||
|
||||
abstract class SocketTest extends TestCase
|
||||
{
|
||||
abstract protected function connect(): Promise;
|
||||
|
||||
public function testAsk()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$question = (new QuestionFactory)->create(Dns\Record::A);
|
||||
$question->setName("google.com");
|
||||
|
||||
/** @var Dns\Internal\Socket $socket */
|
||||
$socket = yield $this->connect();
|
||||
|
||||
/** @var Message $result */
|
||||
$result = yield $socket->ask($question, 5000);
|
||||
|
||||
$this->assertInstanceOf(Message::class, $result);
|
||||
$this->assertSame(MessageTypes::RESPONSE, $result->getType());
|
||||
});
|
||||
}
|
||||
|
||||
public function testGetLastActivity()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$question = (new QuestionFactory)->create(Dns\Record::A);
|
||||
$question->setName("google.com");
|
||||
|
||||
/** @var Dns\Internal\Socket $socket */
|
||||
$socket = yield $this->connect();
|
||||
|
||||
$this->assertLessThan(\time() + 1, $socket->getLastActivity());
|
||||
});
|
||||
}
|
||||
}
|
57
test/TcpSocketTest.php
Normal file
57
test/TcpSocketTest.php
Normal file
@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Delayed;
|
||||
use Amp\Dns;
|
||||
use Amp\Loop;
|
||||
use Amp\Promise;
|
||||
use LibDNS\Messages\Message;
|
||||
use LibDNS\Messages\MessageTypes;
|
||||
use LibDNS\Records\QuestionFactory;
|
||||
use function Amp\Promise\wait;
|
||||
|
||||
class TcpSocketTest extends SocketTest
|
||||
{
|
||||
protected function connect(): Promise
|
||||
{
|
||||
return Dns\Internal\TcpSocket::connect("tcp://8.8.8.8:53");
|
||||
}
|
||||
|
||||
public function testTimeout()
|
||||
{
|
||||
$this->expectException(Dns\TimeoutException::class);
|
||||
wait(Dns\Internal\TcpSocket::connect("tcp://8.8.8.8:53", 0));
|
||||
}
|
||||
|
||||
public function testInvalidUri()
|
||||
{
|
||||
$this->expectException(Dns\DnsException::class);
|
||||
wait(Dns\Internal\TcpSocket::connect("tcp://8.8.8.8"));
|
||||
}
|
||||
|
||||
public function testAfterConnectionTimedOut()
|
||||
{
|
||||
Loop::run(function () {
|
||||
$question = (new QuestionFactory)->create(Dns\Record::A);
|
||||
$question->setName("google.com");
|
||||
|
||||
/** @var Dns\Internal\Socket $socket */
|
||||
$socket = yield $this->connect();
|
||||
|
||||
/** @var Message $result */
|
||||
$result = yield $socket->ask($question, 3000);
|
||||
|
||||
$this->assertInstanceOf(Message::class, $result);
|
||||
$this->assertSame(MessageTypes::RESPONSE, $result->getType());
|
||||
|
||||
// Google's DNS times out really fast
|
||||
yield new Delayed(3000);
|
||||
|
||||
$this->expectException(Dns\DnsException::class);
|
||||
$this->expectExceptionMessageRegExp("(Sending the request failed|Reading from the server failed)");
|
||||
|
||||
yield $socket->ask($question, 3000);
|
||||
});
|
||||
}
|
||||
}
|
21
test/UdpSocketTest.php
Normal file
21
test/UdpSocketTest.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns;
|
||||
use Amp\Promise;
|
||||
use function Amp\Promise\wait;
|
||||
|
||||
class UdpSocketTest extends SocketTest
|
||||
{
|
||||
protected function connect(): Promise
|
||||
{
|
||||
return Dns\Internal\UdpSocket::connect("udp://8.8.8.8:53");
|
||||
}
|
||||
|
||||
public function testInvalidUri()
|
||||
{
|
||||
$this->expectException(Dns\DnsException::class);
|
||||
wait(Dns\Internal\UdpSocket::connect("udp://8.8.8.8"));
|
||||
}
|
||||
}
|
34
test/UnixConfigLoaderTest.php
Normal file
34
test/UnixConfigLoaderTest.php
Normal file
@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Amp\Dns\Test;
|
||||
|
||||
use Amp\Dns\Config;
|
||||
use Amp\Dns\ConfigException;
|
||||
use Amp\Dns\UnixConfigLoader;
|
||||
use Amp\PHPUnit\TestCase;
|
||||
use function Amp\Promise\wait;
|
||||
|
||||
class UnixConfigLoaderTest extends TestCase
|
||||
{
|
||||
public function test()
|
||||
{
|
||||
$loader = new UnixConfigLoader(__DIR__ . "/data/resolv.conf");
|
||||
|
||||
/** @var Config $result */
|
||||
$result = wait($loader->loadConfig());
|
||||
|
||||
$this->assertSame([
|
||||
"127.0.0.1:53",
|
||||
"[2001:4860:4860::8888]:53",
|
||||
], $result->getNameservers());
|
||||
|
||||
$this->assertSame(5000, $result->getTimeout());
|
||||
$this->assertSame(3, $result->getAttempts());
|
||||
}
|
||||
|
||||
public function testNoDefaultsOnConfNotFound()
|
||||
{
|
||||
$this->expectException(ConfigException::class);
|
||||
wait((new UnixConfigLoader(__DIR__ . "/data/non-existent.conf"))->loadConfig());
|
||||
}
|
||||
}
|
2
test/data/hosts
Normal file
2
test/data/hosts
Normal file
@ -0,0 +1,2 @@
|
||||
# localhost
|
||||
127.0.0.1 localhost
|
2
test/data/hosts.invalid.name
Normal file
2
test/data/hosts.invalid.name
Normal file
@ -0,0 +1,2 @@
|
||||
# aaa...aa is too long
|
||||
127.0.0.1 localhost aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
14
test/data/resolv.conf
Normal file
14
test/data/resolv.conf
Normal file
@ -0,0 +1,14 @@
|
||||
# Default
|
||||
nameserver 127.0.0.1
|
||||
|
||||
# Google Fallback
|
||||
nameserver 2001:4860:4860::8888
|
||||
|
||||
# Invalid server gets ignored
|
||||
nameserver foobar
|
||||
|
||||
options timeout 5000
|
||||
options attempts 3
|
||||
|
||||
# Unknown option gets ignored
|
||||
options foo
|
Loading…
Reference in New Issue
Block a user