1
0
mirror of https://github.com/danog/dns.git synced 2024-11-26 20:14:51 +01:00

Massive refactor using amp/1.0.0

This commit is contained in:
Daniel Lowrey 2015-08-01 22:18:44 -04:00
parent 79be6e8c28
commit a2fe7a5764
34 changed files with 601 additions and 1821 deletions

1
.coveralls.yml Normal file
View File

@ -0,0 +1 @@
src_dir: lib

16
.editorconfig Normal file
View File

@ -0,0 +1,16 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = spaces
charset = utf-8
[{.travis.yml}]
indent_style = space
indent_size = 2
[{composer.json}]
indent_style = space
indent_size = 2

9
.gitattributes vendored Normal file
View File

@ -0,0 +1,9 @@
/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

7
.gitignore vendored
View File

@ -1,3 +1,4 @@
.idea /coverage/
vendor /composer.lock
composer.lock /vendor/
/.idea/

15
.php_cs Normal file
View File

@ -0,0 +1,15 @@
<?php
return Symfony\CS\Config\Config::create()
->level(Symfony\CS\FixerInterface::NONE_LEVEL)
->fixers([
"psr2",
"-braces",
"-psr0",
])
->finder(
Symfony\CS\Finder\DefaultFinder::create()
->in(__DIR__ . "/lib")
->in(__DIR__ . "/test")
)
;

View File

@ -1,13 +1,19 @@
sudo: false
language: php language: php
git:
submodules: false
php: php:
- 5.6
- 5.5 - 5.5
- 5.6
- 7
before_script: before_script:
- composer self-update
- composer install - composer install
script: php ./vendor/bin/phpunit --configuration ./phpunit.xml --coverage-text script:
- vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
- php vendor/bin/php-cs-fixer --diff --dry-run -v fix
after_script:
- php vendor/bin/coveralls -v

30
CONTRIBUTING.md Normal file
View 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
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 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.

View File

@ -1,93 +1,41 @@
dns [![Build Status](https://travis-ci.org/amphp/dns.svg?branch=master)](https://travis-ci.org/amphp/dns) # dns
============
Asynchronous DNS resolution built on the [Amp](https://github.com/amphp/amp) concurrency framework [![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)
![Unstable](https://img.shields.io/badge/api-unstable-orange.svg?style=flat-square)
![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)
## Examples `amphp/dns` provides asynchronous DNS name resolution based on the [`amp`](https://github.com/amphp/amp)
concurrency framework.
**Synchronous Resolution Via `Amp\wait()`** **Required PHP Version**
```php - PHP 5.5+
<?php
require __DIR__ . '/vendor/autoload.php';
$resolver = new Amp\Dns\Resolver; **Installation**
$name = 'google.com';
list($address, $type) = Amp\wait($resolver->resolve($name)); ```bash
printf("%s resolved to %s\n", $name, $address); $ composer require amphp/dns:dev-master
``` ```
**Concurrent Synchronous Resolution Via `wait()`** **Example**
```php ```php
<?php <?php
require __DIR__ . '/vendor/autoload.php';
$resolver = new Amp\Dns\Resolver;
$names = [
'github.com',
'google.com',
'stackoverflow.com',
];
$promises = [];
foreach ($names as $name) {
$promises[$name] = $resolver->resolve($name);
}
$comboPromise = Amp\all($promises);
$results = Amp\wait($comboPromise);
foreach ($results as $name => $resultArray) {
list($addr, $type) = $resultArray;
printf("%s => %s\n", $name, $addr);
}
```
**Event Loop Async**
```php
<?php
require __DIR__ . '/vendor/autoload.php'; require __DIR__ . '/vendor/autoload.php';
Amp\run(function () { Amp\run(function () {
$resolver = new Amp\Dns\Resolver; $githubIpv4 = (yield Amp\Dns\resolve("github.com"));
$names = [ var_dump($githubIpv4);
'github.com',
'google.com',
'stackoverflow.com',
'localhost',
'192.168.0.1',
'::1',
];
$promises = []; $googleIpv4 = Amp\Dns\resolve("google.com");
foreach ($names as $name) { $googleIpv6 = Amp\Dns\resolve("google.com", $options = [
$promise = $resolver->resolve($name); "mode" => Amp\Dns\MODE_INET6
$promises[$name] = $promise; ]);
}
// Yield control until the combo promise resolves $firstGoogleResult = (yield Amp\first([$ipv4Result, $ipv6Result]));
list($errors, $successes) = (yield 'some' => $promises); var_dump($firstGoogleResult);
foreach ($names as $name) {
echo isset($errors[$name])
? "FAILED: {$name}\n"
: "{$name} => {$successes[$name][0]}\n";
}
// Stop the event loop so we don't sit around forever
Amp\stop();
}); });
``` ```
## Tests
[![Build Status](https://travis-ci.org/amphp/dns.svg?branch=master)](https://travis-ci.org/amphp/dns)
Tests can be run from the command line using:
`php vendor/bin/phpunit -c phpunit.xml`
or to exclude tests that require a working internet connection:
`php vendor/bin/phpunit -c phpunit.xml --exclude-group internet`

View File

@ -1,36 +1,38 @@
{ {
"name": "amphp/dns", "name": "amphp/dns",
"homepage": "https://github.com/amphp/dns", "homepage": "https://github.com/amphp/dns",
"description": "Asynchronous DNS resolution built on the Amp concurrency framework", "description": "Async DNS resolution built on the amp concurrency framework",
"keywords": ["dns", "resolve", "client", "async", "amp"], "keywords": ["dns", "resolve", "client", "async", "amp"],
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
{ {
"name": "Chris Wright", "name": "Chris Wright",
"email": "addr@daverandom.com", "email": "addr@daverandom.com"
"role": "Creator / Lead Developer" },
{
"name": "Daniel Lowrey",
"email": "rdlowrey@php.net"
} }
], ],
"require": { "require": {
"php": ">=5.5", "php": ">=5.5",
"amphp/amp": "^1", "amphp/amp": "^1",
"amphp/cache": "dev-master", "amphp/cache": "dev-master",
"amphp/filesystem": "dev-master",
"daverandom/libdns": "~1.0" "daverandom/libdns": "~1.0"
}, },
"require-dev": { "require-dev": {
"mockery/mockery": ">=0.9.1", "phpunit/phpunit": "~4.4.0",
"phpunit/phpunit": "~4.1.0" "fabpot/php-cs-fixer": "~1.9",
"satooshi/php-coveralls": "dev-master"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Amp\\Dns\\": "lib" "Amp\\Dns\\": "lib"
}, },
"files": ["lib/functions.php"] "files": [
}, "lib/constants.php",
"autoload-dev": { "lib/functions.php"
"psr-4": { ]
"Amp\\Dns\\Test\\": "test/"
},
"files": ["test/bootstrap.php"]
} }
} }

View File

@ -1,9 +0,0 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
$name = 'google.com';
$resolver = new Amp\Dns\Resolver;
$promise = $resolver->resolve($name);
list($address, $type) = $promise->wait();
printf("%s resolved to %s\n", $name, $address);

View File

@ -1,34 +0,0 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
Amp\run(function() {
$names = [
'github.com',
'google.com',
'stackoverflow.com',
'localhost',
'192.168.0.1',
'::1',
];
$promises = [];
$resolver = new Amp\Dns\Resolver;
foreach ($names as $name) {
$promise = $resolver->resolve($name);
$promises[$name] = $promise;
}
// Combine our multiple promises into a single promise
$comboPromise = Amp\some($promises);
// Yield control until the combo promise resolves
list($errors, $successes) = (yield $comboPromise);
foreach ($names as $name) {
echo isset($errors[$name]) ? "FAILED: {$name}\n" : "{$name} => {$successes[$name][0]}\n";
}
// Stop the event loop so we don't sit around forever
Amp\stop();
});

View File

@ -1,12 +0,0 @@
<?php
namespace Amp\Dns;
class AddressModes {
const INET4_ADDR = 0b0001;
const INET6_ADDR = 0b0010;
const PREFER_INET6 = 0b0100;
const ANY_PREFER_INET4 = 0b0011;
const ANY_PREFER_INET6 = 0b0111;
const CNAME = 0b1000;
}

View File

@ -1,382 +0,0 @@
<?php
namespace Amp\Dns;
use Amp\Reactor;
use Amp\Failure;
use Amp\Deferred;
class Client {
const OP_MS_REQUEST_TIMEOUT = 0b0001;
const OP_SERVER_ADDRESS = 0b0010;
const OP_SERVER_PORT = 0b0011;
/**
* @var \Amp\Reactor
*/
private $reactor;
/**
* @var \Amp\Dns\RequestBuilder
*/
private $requestBuilder;
/**
* @var \Amp\Dns\ResponseInterpreter
*/
private $responseInterpreter;
/**
* @var \Amp\Dns\Cache
*/
private $cache;
/**
* @var resource
*/
private $socket;
/**
* @var int
*/
private $msRequestTimeout = 2000;
/**
* @var int
*/
private $readWatcherId;
/**
* @var array
*/
private $pendingLookups = [];
/**
* @var array
*/
private $pendingRequestsByNameAndType = [];
/**
* @var array
*/
private $pendingRequestsById = [];
/**
* @var int
*/
private $requestIdCounter = 0;
/**
* @var int
*/
private $lookupIdCounter = 0;
/**
* @var string
*/
private $serverAddress = '8.8.8.8';
/**
* @var int
*/
private $serverPort = 53;
/**
* @var bool
*/
private $isReadWatcherEnabled = false;
/**
* @param \Amp\Reactor $reactor
* @param \Amp\Dns\RequestBuilder $requestBuilder
* @param \Amp\Dns\ResponseInterpreter $responseInterpreter
* @param \Amp\Dns\Cache $cache
*/
public function __construct(
Reactor $reactor = null,
RequestBuilder $requestBuilder = null,
ResponseInterpreter $responseInterpreter = null,
Cache $cache = null
) {
$this->reactor = $reactor ?: \Amp\reactor();
$this->requestBuilder = $requestBuilder ?: new RequestBuilder;
$this->responseInterpreter = $responseInterpreter ?: new ResponseInterpreter;
$this->cache = $cache ?: (new CacheFactory)->select();
}
/**
* Resolve a name from a DNS server
*
* @param string $name
* @param int $mode
* @return \Amp\Promise
*/
public function resolve($name, $mode) {
// Defer UDP server connect until needed to allow custom address/port option assignment
// after object instantiation.
if (empty($this->socket) && !$this->connect()) {
return new Failure(new ResolutionException(
sprintf(
"Failed connecting to DNS server at %s:%d",
$this->serverAddress,
$this->serverPort
)
));
}
if (!$this->isReadWatcherEnabled) {
$this->isReadWatcherEnabled = true;
$this->reactor->enable($this->readWatcherId);
}
$promisor = new Deferred;
$id = $this->getNextFreeLookupId();
$this->pendingLookups[$id] = [
'name' => $name,
'requests' => $this->getRequestList($mode),
'last_type' => null,
'future' => $promisor,
];
$this->processPendingLookup($id);
return $promisor->promise();
}
private function connect() {
$address = sprintf('udp://%s:%d', $this->serverAddress, $this->serverPort);
if (!$this->socket = @stream_socket_client($address, $errNo, $errStr)) {
return false;
}
stream_set_blocking($this->socket, 0);
$this->readWatcherId = $this->reactor->onReadable($this->socket, function() {
$this->onReadableSocket();
}, ["enable" => false]);
return true;
}
private function getNextFreeLookupId() {
do {
$result = $this->lookupIdCounter++;
if ($this->lookupIdCounter >= PHP_INT_MAX) {
$this->lookupIdCounter = 0;
}
} while(isset($this->pendingLookups[$result]));
return $result;
}
private function getRequestList($mode) {
$result = [];
if ($mode & AddressModes::PREFER_INET6) {
if ($mode & AddressModes::INET6_ADDR) {
$result[] = AddressModes::INET6_ADDR;
}
if ($mode & AddressModes::INET4_ADDR) {
$result[] = AddressModes::INET4_ADDR;
}
} else {
if ($mode & AddressModes::INET4_ADDR) {
$result[] = AddressModes::INET4_ADDR;
}
if ($mode & AddressModes::INET6_ADDR) {
$result[] = AddressModes::INET6_ADDR;
}
}
return $result;
}
private function getNextFreeRequestId() {
do {
$result = $this->requestIdCounter++;
if ($this->requestIdCounter >= 65536) {
$this->requestIdCounter = 0;
}
} while (isset($this->pendingRequestsById[$result]));
return $result;
}
private function sendRequest($request) {
$packet = $this->requestBuilder->buildRequest($request['id'], $request['name'], $request['type']);
$bytesWritten = fwrite($this->socket, $packet);
if ($bytesWritten < strlen($packet)) {
$this->completeRequest($request, null, ResolutionErrors::ERR_REQUEST_SEND_FAILED);
return;
}
$request['timeout_id'] = $this->reactor->once(function() use($request) {
unset($this->pendingRequestsByNameAndType[$request['name']][$request['type']]);
$this->completeRequest($request, null, ResolutionErrors::ERR_SERVER_TIMEOUT);
}, $this->msRequestTimeout);
$this->pendingRequestsById[$request['id']] = $request;
$this->pendingRequestsByNameAndType[$request['name']][$request['type']] = &$this->pendingRequestsById[$request['id']];
}
private function onReadableSocket() {
$packet = fread($this->socket, 512);
// Decode the response and clean up the pending requests list
$decoded = $this->responseInterpreter->decode($packet);
if ($decoded === null) {
return;
}
list($id, $response) = $decoded;
$request = $this->pendingRequestsById[$id];
$name = $request['name'];
$this->reactor->cancel($request['timeout_id']);
unset(
$this->pendingRequestsById[$id],
$this->pendingRequestsByNameAndType[$name][$request['type']]
);
// Interpret the response and make sure we have at least one resource record
$interpreted = $this->responseInterpreter->interpret($response, $request['type']);
if ($interpreted === null) {
foreach ($request['lookups'] as $id => $lookup) {
$this->processPendingLookup($id);
}
return;
}
// Distribute the result to the appropriate lookup routine
list($type, $addr, $ttl) = $interpreted;
if ($type === AddressModes::CNAME) {
foreach ($request['lookups'] as $id => $lookup) {
$this->redirectPendingLookup($id, $addr);
}
} else if ($addr !== null) {
$this->cache->set($key, $addr, $ttl);
$this->completeRequest($request, $addr, $type);
} else {
foreach ($request['lookups'] as $id => $lookup) {
$this->processPendingLookup($id);
}
}
}
private function completePendingLookup($id, $addr, $type) {
if (!isset($this->pendingLookups[$id])) {
return;
}
$lookupStruct = $this->pendingLookups[$id];
$future = $lookupStruct['future'];
unset($this->pendingLookups[$id]);
if ($addr) {
$future->succeed([$addr, $type]);
} else {
$future->fail(new ResolutionException(
$msg = sprintf('DNS resolution failed: %s', $lookupStruct['name']),
$code = $type
));
}
if (empty($this->pendingLookups)) {
$this->isReadWatcherEnabled = false;
$this->reactor->disable($this->readWatcherId);
}
}
private function completeRequest($request, $addr, $type) {
foreach ($request['lookups'] as $id => $lookup) {
$this->completePendingLookup($id, $addr, $type);
}
}
private function processPendingLookup($id) {
if (!$this->pendingLookups[$id]['requests']) {
$this->completePendingLookup($id, null, ResolutionErrors::ERR_NO_RECORD);
return;
}
$name = $this->pendingLookups[$id]['name'];
$type = array_shift($this->pendingLookups[$id]['requests']);
$key = $name . $type;
if (yield $this->cache->has($key)) {
$this->completePendingLookup($id, $addr, $type);
} else {
$this->dispatchRequest($id, $name, $type);
}
}
private function dispatchRequest($id, $name, $type) {
$this->pendingLookups[$id]['last_type'] = $type;
$this->pendingRequestsByNameAndType[$name][$type]['lookups'][$id] = $this->pendingLookups[$id];
if (count($this->pendingRequestsByNameAndType[$name][$type]) === 1) {
$request = [
'id' => $this->getNextFreeRequestId(),
'name' => $name,
'type' => $type,
'lookups' => [$id => $this->pendingLookups[$id]],
'timeout_id' => null,
];
$this->sendRequest($request);
}
}
private function redirectPendingLookup($id, $name) {
array_unshift($this->pendingLookups[$id]['requests'], $this->pendingLookups[$id]['last_type']);
$this->pendingLookups[$id]['last_type'] = null;
$this->pendingLookups[$id]['name'] = $name;
$this->processPendingLookup($id);
}
/**
* Set the Client options
*
* @param int $option
* @param mixed $value
* @throws \RuntimeException If modifying server address/port once connected
* @throws \DomainException On unknown option key
* @return self
*/
public function setOption($option, $value) {
switch ($option) {
case self::OP_MS_REQUEST_TIMEOUT:
$this->msRequestTimeout = (int) $value;
break;
case self::OP_SERVER_ADDRESS:
if ($this->socket) {
throw new \RuntimeException(
'Server address cannot be modified once connected'
);
} else {
$this->serverAddress = $value;
}
break;
case self::OP_SERVER_PORT:
if ($this->socket) {
throw new \RuntimeException(
'Server port cannot be modified once connected'
);
} else {
$this->serverPort = $value;
}
break;
default:
throw new \DomainException(
sprintf("Unkown option: %s", $option)
);
}
return $this;
}
}

View File

@ -1,120 +0,0 @@
<?php
namespace Amp\Dns;
class HostsFile {
/**
* Path to hosts file
*
* @var string
*/
private $path;
/**
* Mapped names from hosts file
*
* @var array
*/
private $data;
/**
* The file modification time when the last reload was performed
*
* @var int
*/
private $mtime = 0;
/**
* Constructor
*
* @param string $path
* @throws \LogicException
*/
public function __construct($path = null) {
if ($path === null) {
$path = \stripos(PHP_OS, "win") === 0
? "C:\Windows\system32\drivers\etc\hosts"
: "/etc/hosts"
;
}
if (!file_exists($path)) {
throw new \LogicException($path . ' does not exist');
} else if (!is_file($path)) {
throw new \LogicException($path . ' is not a file');
} else if (!is_readable($path)) {
throw new \LogicException($path . ' is not readable');
}
$this->path = $path;
}
private function reload() {
$this->data = [
AddressModes::INET4_ADDR => [],
AddressModes::INET6_ADDR => [],
];
$key = null;
foreach (file($this->path) as $line) {
$line = trim($line);
if ($line !== '' && $line[0] === '#') {
continue;
}
$parts = preg_split('/\s+/', $line);
if (!($ip = @inet_pton($parts[0]))) {
continue;
} elseif (isset($ip[4])) {
$key = AddressModes::INET6_ADDR;
} else {
$key = AddressModes::INET4_ADDR;
}
for ($i = 1, $l = count($parts); $i < $l; $i++) {
if (validateHostName($parts[$i])) {
$this->data[$key][$parts[$i]] = $parts[0];
}
}
}
}
private function ensureDataIsCurrent() {
clearstatcache(true, $this->path);
$modTime = filemtime($this->path);
if ($modTime > $this->mtime) {
$this->reload();
$this->mtime = $modTime;
}
}
/**
* Look up a name in the hosts file
*
* @param string $name
* @param int $mode
* @return array|null
*/
public function resolve($name, $mode) {
$this->ensureDataIsCurrent();
$have4 = isset($this->data[AddressModes::INET4_ADDR][$name]);
$have6 = isset($this->data[AddressModes::INET6_ADDR][$name]);
$want4 = (bool)($mode & AddressModes::INET4_ADDR);
$want6 = (bool)($mode & AddressModes::INET6_ADDR);
$pref6 = (bool)($mode & AddressModes::PREFER_INET6);
if ($have6 && $want6 && (!$want4 || !$have4 || $pref6)) {
return [$this->data[AddressModes::INET6_ADDR][$name], AddressModes::INET6_ADDR];
} else if ($have4 && $want4) {
return [$this->data[AddressModes::INET4_ADDR][$name], AddressModes::INET4_ADDR];
}
return null;
}
public function getPath() {
return $this->path;
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace Amp\Dns;
class NoRecordException extends ResolutionException {}

View File

@ -1,66 +0,0 @@
<?php
namespace Amp\Dns;
use LibDNS\Messages\MessageFactory;
use LibDNS\Messages\MessageTypes;
use LibDNS\Records\QuestionFactory;
use LibDNS\Records\ResourceQTypes;
use LibDNS\Encoder\EncoderFactory;
use LibDNS\Encoder\Encoder;
class RequestBuilder {
/**
* @var \LibDNS\Messages\MessageFactory
*/
private $messageFactory;
/**
* @var \LibDNS\Records\QuestionFactory
*/
private $questionFactory;
/**
* @var \LibDNS\Encoder\Encoder
*/
private $encoder;
/**
* Constructor
*
* @param MessageFactory $messageFactory
* @param QuestionFactory $questionFactory
* @param Encoder $encoder
*/
public function __construct(
MessageFactory $messageFactory = null,
QuestionFactory $questionFactory = null,
Encoder $encoder = null
) {
$this->messageFactory = $messageFactory ?: new MessageFactory;
$this->questionFactory = $questionFactory ?: new QuestionFactory;
$this->encoder = $encoder ?: (new EncoderFactory)->create();
}
/**
* Build a request packet for a name and record type
*
* @param int $id
* @param string $name
* @param int $type
* @return string
*/
public function buildRequest($id, $name, $type) {
$qType = $type === AddressModes::INET4_ADDR ? ResourceQTypes::A : ResourceQTypes::AAAA;
$question = $this->questionFactory->create($qType);
$question->setName($name);
$request = $this->messageFactory->create(MessageTypes::QUERY);
$request->setID($id);
$request->getQuestionRecords()->add($question);
$request->isRecursionDesired(true);
return $this->encoder->encode($request);
}
}

View File

@ -1,10 +0,0 @@
<?php
namespace Amp\Dns;
class ResolutionErrors {
const ERR_INVALID_NAME = 1;
const ERR_NO_RECORD = 2;
const ERR_SERVER_TIMEOUT = 3;
const ERR_REQUEST_SEND_FAILED = 4;
}

View File

@ -1,77 +0,0 @@
<?php
namespace Amp\Dns;
use Amp\Success;
use Amp\Failure;
class Resolver {
/**
* @var \Amp\Dns\Client
*/
private $client;
/**
* @var \Amp\Dns\HostsFile
*/
private $hostsFile;
/**
* @var \Amp\Dns\NameValidator
*/
private $nameValidator;
/**
* @param \Amp\Dns\Client $client
* @param \Amp\Dns\HostsFile $hostsFile
* @param \Amp\Dns\NameValidator $nameValidator
*/
public function __construct(
Client $client = null,
HostsFile $hostsFile = null,
NameValidator $nameValidator = null
) {
$this->client = $client ?: new Client;
$this->nameValidator = $nameValidator ?: new NameValidator;
$this->hostsFile = $hostsFile ?: new HostsFile($this->nameValidator);
}
/**
* Resolve a host name to an IP address
*
* @param string $name
* @param int $mode
* @return \Amp\Promise
*/
public function resolve($name, $mode = AddressModes::ANY_PREFER_INET4) {
if (strcasecmp($name, 'localhost') === 0) {
return new Success($this->resolveLocalhost($mode));
} elseif ($addrStruct = $this->resolveFromIp($name)) {
return new Success($addrStruct);
} elseif (!$this->nameValidator->validate($name)) {
return new Failure(new ResolutionException(
sprintf('Invalid DNS name format: %s', $name)
));
} elseif ($this->hostsFile && ($addrStruct = $this->hostsFile->resolve($name, $mode))) {
return new Success($addrStruct);
} else {
return $this->client->resolve($name, $mode);
}
}
private function resolveLocalhost($mode) {
return ($mode & AddressModes::PREFER_INET6)
? ['::1', AddressModes::INET6_ADDR]
: ['127.0.0.1', AddressModes::INET4_ADDR];
}
private function resolveFromIp($name) {
if (!$inAddr = @inet_pton($name)) {
return [];
} elseif (isset($inAddr['15'])) {
return [$name, AddressModes::INET6_ADDR];
} else {
return [$name, AddressModes::INET4_ADDR];
}
}
}

View File

@ -1,83 +0,0 @@
<?php
namespace Amp\Dns;
use LibDNS\Decoder\Decoder;
use LibDNS\Decoder\DecoderFactory;
use LibDNS\Messages\Message;
use LibDNS\Messages\MessageTypes;
use LibDNS\Records\ResourceTypes;
class ResponseInterpreter {
/**
* @var Decoder
*/
private $decoder;
/**
* Constructor
*
* @param Decoder $decoder
*/
public function __construct(Decoder $decoder = null) {
$this->decoder = $decoder ?: (new DecoderFactory)->create();
}
/**
* Attempt to decode a data packet to a DNS response message
*
* @param string $packet
* @return Message|null
*/
public function decode($packet) {
try {
$message = $this->decoder->decode($packet);
} catch (\Exception $e) {
return null;
}
if ($message->getType() !== MessageTypes::RESPONSE || $message->getResponseCode() !== 0) {
return null;
}
return [$message->getID(), $message];
}
/**
* Extract the message ID and response data from a DNS response packet
*
* @param Message $message
* @param int $expectedType
* @return array|null
*/
public function interpret($message, $expectedType) {
static $typeMap = [
AddressModes::INET4_ADDR => ResourceTypes::A,
AddressModes::INET6_ADDR => ResourceTypes::AAAA,
];
$answers = $message->getAnswerRecords();
if (!count($answers)) {
return null;
}
/** @var \LibDNS\Records\Resource $record */
$cname = null;
foreach ($answers as $record) {
switch ($record->getType()) {
case $typeMap[$expectedType]:
return [$expectedType, (string)$record->getData(), $record->getTTL()];
case ResourceTypes::CNAME:
$cname = (string)$record->getData();
break;
}
}
if ($cname) {
return [AddressModes::CNAME, $cname, null];
}
return null;
}
}

5
lib/TimeoutException.php Normal file
View File

@ -0,0 +1,5 @@
<?php
namespace Amp\Dns;
class TimeoutException extends ResolutionException {}

14
lib/constants.php Normal file
View File

@ -0,0 +1,14 @@
<?php
namespace Amp\Dns;
// @codeCoverageIgnoreStart
const DEFAULT_SERVER = "8.8.8.8";
const DEFAULT_PORT = 53;
const DEFAULT_TIMEOUT = 5000;
const MAX_REQUEST_ID = 65536;
const IDLE_TIMEOUT = 15000;
const MODE_INET4 = 1;
const MODE_INET6 = 2;
const MODE_CNAME = 3;
// @codeCoverageIgnoreEnd

View File

@ -2,125 +2,423 @@
namespace Amp\Dns; namespace Amp\Dns;
use Amp\Reactor; use \LibDNS\Messages\MessageFactory;
use \LibDNS\Messages\MessageTypes;
use \LibDNS\Records\QuestionFactory;
use \LibDNS\Records\ResourceTypes;
use \LibDNS\Records\ResourceQTypes;
use \LibDNS\Encoder\EncoderFactory;
use \LibDNS\Decoder\DecoderFactory;
function validateHostName($name) { /**
if (isset($name[253])) { * Resolve a DNS name to an IP address
return false; *
* Upon success the returned promise resolves to an indexed array of the form:
*
* [string $resolvedIp, int $mode, int $ttl]
*
* The $mode parameter at index 1 corresponds to one of two constant values to
* indicate if the resulting IP is IPv4 or IPv6:
*
* - Amp\Dns\MODE_INET4
* - Amp\Dns\MODE_INET6
*
* A null $ttl value indicates the DNS name was resolved from the cache or the
* local hosts file.
*
* Options:
*
* - "server" | string Custom DNS server address in ip or ip:port format
* - "timeout" | int Default: 3000ms
* - "mode" | int Either Amp\Dns\MODE_INET4 or Amp\Dns\MODE_INET6
* - "no_hosts" | bool Ignore entries in the hosts file
* - "no_cache" | bool Ignore cached DNS response entries
*
* If the custom per-request "server" option is not present the resolver will
* use the default from the following built-in constant:
*
* - Amp\Dns\DEFAULT_SERVER
*
* @param string $name The hostname to resolve
* @param array $options
* @return \Amp\Promise
* @TODO add boolean "clear_cache" option flag
* @TODO add boolean "reload_hosts" option flag
*/
function resolve($name, array $options = []) {
$mode = isset($options["mode"]) ? $options["mode"] : MODE_INET4;
switch ($mode) {
case MODE_INET4:
case MODE_INET6:
break;
default:
return new \Amp\Failure(new ResolutionException(
"Invalid request mode option; Amp\Dns\MODE_INET4 or Amp\Dns\MODE_INET6 required"
));
}
if (\strcasecmp($name, "localhost") === 0) {
return new \Amp\Success(($mode === MODE_INET6)
? ["::1", MODE_INET6, $ttl = null]
: ["127.0.0.1", MODE_INET4, $ttl = null]
);
} elseif (!$inAddr = @\inet_pton($name)) {
return __isValidHostName($name)
? \Amp\resolve(__doResolve($name, $mode, $options))
: new \Amp\Failure(new ResolutionException(
"Cannot resolve; invalid host name"
))
;
} elseif (isset($inAddr[15])) {
return new \Amp\Success([$name, MODE_INET6, $ttl = null]);
} else {
return new \Amp\Success([$name, MODE_INET4, $ttl = null]);
}
} }
function __isValidHostName($name) {
$pattern = "/^(?:[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9]){0,1})(?:\.[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])*$/i"; $pattern = "/^(?:[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9]){0,1})(?:\.[a-z0-9][a-z0-9\-]{0,61}[a-z0-9])*$/i";
return (bool) \preg_match($pattern, $name); return isset($name[253]) ? false : (bool) \preg_match($pattern, $name);
} }
function __doResolve($name, $mode, $options) {
static $state;
$state = $state ?: (yield \Amp\resolve(__init()));
$name = \strtolower($name);
// Check for cache hits
class DnsServer { $cacheKey = "{$mode}#{$name}";
private $addr; if (empty($options["no_cache"])) {
private static $addresses = []; if (yield $state->arrayCache->has($cacheKey)) {
$result = (yield $state->arrayCache->get($cacheKey));
public function __construct($addr, $port = 53) { yield new \Amp\CoroutineResult([$result, $mode, $ttl = null]);
if (!$inAddr = @\inet_pton($addr)) { return;
throw new \DomainException(
"Invalid IP address: {$addr}"
);
}
$addr = \inet_ntop($inAddr);
if (isset($inAddr[15])) {
$addr = "[{$addr}]";
}
$port = (int) $port;
$this->addr = "udp://{$addr}:{$port}";
} }
} }
function initServer() // Check for hosts file matches
if (empty($options["no_hosts"])) {
function resolver(Reactor $reactor $have4 = isset($state->hostsFile[MODE_INET4][$name]);
$have6 = isset($state->hostsFile[MODE_INET6][$name]);
class Resolver { $want4 = (bool)($mode & MODE_INET4);
private $ $want6 = (bool)($mode & MODE_INET6);
} if ($have6 && $want6) {
$result = [$state->hostsFile[MODE_INET6][$name], MODE_INET6, $ttl = null];
function resolve($name, array $options = []) { } elseif ($have4 && $want4) {
$generator = __doResolve($name, $options); $result = [$state->hostsFile[MODE_INET4][$name], MODE_INET4, $ttl = null];
return \Amp\resolve($generator);
}
function __doResolve($name, $options) {
static $lookupIdCounter;
static $cache;
if (empty($cache)) {
$cache = new \Amp\Cache\ArrayCache;
}
$mode = isset($options["mode"]) ? $options["mode"] : AddressModes::ANY_PREFER_INET4;
if ($mode & AddressModes::PREFER_INET6) {
if ($mode & AddressModes::INET6_ADDR) {
$requests[] = AddressModes::INET6_ADDR;
}
if ($mode & AddressModes::INET4_ADDR) {
$requests[] = AddressModes::INET4_ADDR;
}
} else { } else {
if ($mode & AddressModes::INET4_ADDR) { $result = null;
$requests[] = AddressModes::INET4_ADDR;
} }
if ($mode & AddressModes::INET6_ADDR) { if ($result) {
$requests[] = AddressModes::INET6_ADDR;
}
}
$type = array_shift($requests);
$cacheKey = $name . $type;
if (yield $cache->has($cacheKey)) {
$result = yield $cache->get($cacheKey);
yield new \Amp\CoroutineResult($result); yield new \Amp\CoroutineResult($result);
return; return;
} }
do {
$lookupId = $lookupIdCounter++;
if ($lookupIdCounter >= PHP_INT_MAX) {
$lookupIdCounter = 0;
} }
} while (isset($pendingLookups[$lookupId]));
$remote = isset($options["server_addr"]) $timeout = empty($options["timeout"]) ? DEFAULT_TIMEOUT : (int) $options["timeout"];
? "udp://" . $options["server_addr"]
: "udp://8.8.8.8:53" $uri = empty($options["server"])
? "udp://" . DEFAULT_SERVER . ":" . DEFAULT_PORT
: __parseCustomServerUri($options["server"])
; ;
$server = __loadExistingServer($state, $uri) ?: __loadNewServer($state, $uri);
if (!$socket = @\stream_socket_client($remote, $errno, $errstr)) { // Get the next available request ID
throw new ConnectException(sprintf( do {
$requestId = $state->requestIdCounter++;
if ($state->requestIdCounter >= MAX_REQUEST_ID) {
$state->requestIdCounter = 1;
}
} while (isset($state->pendingRequests[$requestId]));
// Create question record
$questionType = ($mode === MODE_INET4) ? ResourceQTypes::A : ResourceQTypes::AAAA;
$question = $state->questionFactory->create($questionType);
$question->setName($name);
// Create request message
$request = $state->messageFactory->create(MessageTypes::QUERY);
$request->getQuestionRecords()->add($question);
$request->isRecursionDesired(true);
$request->setID($requestId);
// Encode request message
$requestPacket = $state->encoder->encode($request);
// Send request
$bytesWritten = \fwrite($server->socket, $requestPacket);
if ($bytesWritten === false || isset($packet[$bytesWritten])) {
throw new ResolutionException(
"Request send failed"
);
}
$promisor = new \Amp\Deferred;
$server->pendingRequests[$requestId] = true;
$state->pendingRequests[$requestId] = [$promisor, $name, $mode];
try {
$resultArr = (yield \Amp\timeout($promisor->promise(), $timeout));
} catch (\Amp\TimeoutException $e) {
throw new TimeoutException(
"Name resolution timed out for {$name}"
);
}
list($resultIp, $resultMode, $resultTtl) = $resultArr;
if ($resultMode === MODE_CNAME) {
$result = (yield resolve($resultIp, $mode, $options));
list($resultIp, $resultMode, $resultTtl) = $result;
}
yield $state->arrayCache->set($cacheKey, $resultIp, $resultTtl);
yield new \Amp\CoroutineResult($resultArr);
}
function __init() {
$state = new \StdClass;
$state->messageFactory = new MessageFactory;
$state->questionFactory = new QuestionFactory;
$state->encoder = (new EncoderFactory)->create();
$state->decoder = (new DecoderFactory)->create();
$state->arrayCache = new \Amp\Cache\ArrayCache;
$state->hostsFile = (yield \Amp\resolve(__loadHostsFile()));
$state->requestIdCounter = 1;
$state->pendingRequests = [];
$state->serverIdMap = [];
$state->serverUriMap = [];
$state->serverIdTimeoutMap = [];
$state->now = \time();
$state->serverTimeoutWatcher = \Amp\repeat(function ($watcherId) use ($state) {
$state->now = $now = \time();
foreach ($state->serverIdTimeoutMap as $id => $expiry) {
if ($now > $expiry) {
__unloadServer($state, $id);
}
}
if (empty($state->serverIdMap)) {
\Amp\disable($watcherId);
}
}, 1000, $options = [
"enable" => true,
"keep_alive" => false,
]);
yield new \Amp\CoroutineResult($state);
}
function __loadHostsFile($path = null) {
$data = [
MODE_INET4 => [],
MODE_INET6 => [],
];
if (empty($path)) {
$path = \stripos(PHP_OS, "win") === 0
? "C:\\Windows\\system32\\drivers\\etc\\hosts"
: "/etc/hosts"
;
}
try {
$contents = (yield \Amp\Filesystem\get($path));
$key = null;
$lines = \array_filter(\array_map("trim", \explode("\n", $contents)));
foreach ($lines as $line) {
if ($line[0] === "#") {
continue;
}
$parts = \preg_split('/\s+/', $line);
if (!($ip = @\inet_pton($parts[0]))) {
continue;
} elseif (isset($ip[4])) {
$key = MODE_INET6;
} else {
$key = MODE_INET4;
}
for ($i = 1, $l = \count($parts); $i < $l; $i++) {
if (__isValidHostName($parts[$i])) {
$data[$key][$parts[$i]] = $parts[0];
}
}
}
} catch (\Exception $e) {
// hosts file doesn't exist
}
yield new \Amp\CoroutineResult($data);
}
function __parseCustomServerUri($uri) {
if (!\is_string($uri)) {
throw new ResolutionException(
"Invalid server address (". gettype($uri) ."); string IP required"
);
}
if (($colonPos = strrpos(":", $uri)) !== false) {
$addr = \substr($uri, 0, $colonPos);
$port = \substr($uri, $colonPos);
} else {
$addr = $uri;
$port = DEFAULT_PORT;
}
$addr = trim($addr, "[]");
if (!$inAddr = @\inet_pton($addr)) {
throw new ResolutionException(
"Invalid server URI; IP address required"
);
}
return isset($inAddr[15]) ? "udp://[{$addr}]:{$port}" : "udp://{$addr}:{$port}";
}
function __loadExistingServer($state, $uri) {
if (empty($state->serverUriMap[$uri])) {
return;
}
$server = $state->serverUriMap[$uri];
if (\is_resource($server->socket) && !@\feof($server->socket)) {
unset($state->serverIdTimeoutMap[$server->id]);
\Amp\enable($server->watcherId);
return $server;
}
__unloadServer($state, $server->id);
}
function __loadNewServer($state, $uri) {
if (!$socket = @\stream_socket_client($uri, $errno, $errstr)) {
throw new ResolutionException(sprintf(
"Connection to %s failed: [Error #%d] %s", "Connection to %s failed: [Error #%d] %s",
$uri, $uri,
$errno, $errno,
$errstr $errstr
)); ));
} }
\stream_set_blocking($socket, false); \stream_set_blocking($socket, false);
$id = (int) $socket;
$server = new \StdClass;
$server->id = $id;
$server->uri = $uri;
$server->socket = $socket;
$server->pendingRequests = [];
$server->watcherId = \Amp\onReadable($socket, "Amp\Dns\__onReadable", [
"enable" => true,
"keep_alive" => true,
"cb_data" => $state,
]);
$state->serverIdMap[$id] = $server;
$state->serverUriMap[$uri] = $server;
return $server;
}
function __unloadServer($state, $serverId, $error = null) {
$server = $state->serverIdMap[$serverId];
\Amp\cancel($server->watcherId);
unset(
$state->serverIdMap[$serverId],
$state->serverUriMap[$server->uri]
);
if (\is_resource($server->socket)) {
@\fclose($server->socket);
}
if ($error && $server->pendingRequests) {
foreach (array_keys($server->pendingRequests) as $requestId) {
list($promisor) = $state->pendingRequests[$requestId];
$promisor->fail($error);
}
}
}
if (yield $this->cache->has($cacheKey)) { function __onReadable($watcherId, $socket, $state) {
$this->completePendingLookup($id, $addr, $type); $serverId = (int) $socket;
$packet = @\fread($socket, 512);
if ($packet != "") {
__decodeResponsePacket($state, $serverId, $packet);
} else { } else {
$this->dispatchRequest($id, $name, $type); __unloadServer($state, $serverId, new ResolutionException(
"Server connection failed"
));
}
} }
$promisor = new Deferred; function __decodeResponsePacket($state, $serverId, $packet) {
$reactor->onReadable($socket, "Amp\Dns\__onReadable", $promisor); try {
yield $promisor->promise(); $response = $state->decoder->decode($packet);
$requestId = $response->getID();
$responseCode = $response->getResponseCode();
$responseType = $response->getType();
if ($responseCode !== 0) {
__finalizeResult($state, $serverId, $requestId, new ResolutionException(
"Server returned error code: {$responseCode}"
));
} elseif ($responseType !== MessageTypes::RESPONSE) {
__unloadServer($state, $serverId, new ResolutionException(
"Invalid server reply; expected RESPONSE but received QUERY"
));
} else {
__processDecodedResponse($state, $serverId, $requestId, $response);
}
} catch (\Exception $e) {
__unloadServer($state, $serverId, new ResolutionException(
"Response decode error",
0,
$e
));
}
} }
function __getRequestModesFrom function __processDecodedResponse($state, $serverId, $requestId, $response) {
static $typeMap = [
MODE_INET4 => ResourceTypes::A,
MODE_INET6 => ResourceTypes::AAAA,
];
list($promisor, $name, $mode) = $state->pendingRequests[$requestId];
$answers = $response->getAnswerRecords();
foreach ($answers as $record) {
switch ($record->getType()) {
case $typeMap[$mode]:
$result = [(string) $record->getData(), $mode, $record->getTTL()];
break 2;
case ResourceTypes::CNAME:
// CNAME should only be used if no A records exist so we only
// break out of the switch (and not the foreach loop) here.
$result = [(string) $record->getData(), MODE_CNAME, $record->getTTL()];
break;
}
}
if (empty($result)) {
$recordType = ($mode === MODE_INET4) ? "A" : "AAAA";
__finalizeResult($state, $serverId, $requestId, new NoRecordException(
"No {$recordType} records returned for {$name}"
));
} else {
__finalizeResult($state, $serverId, $requestId, $error = null, $result);
}
}
function __finalizeResult($state, $serverId, $requestId, $error = null, $result = null) {
if (empty($state->pendingRequests[$requestId])) {
return;
}
list($promisor) = $state->pendingRequests[$requestId];
$server = $state->serverIdMap[$serverId];
unset(
$state->pendingRequests[$requestId],
$server->pendingRequests[$requestId]
);
if (empty($server->pendingRequests)) {
$state->serverIdTimeoutMap[$server->id] = $state->now + IDLE_TIMEOUT;
\Amp\disable($server->watcherId);
\Amp\enable($state->serverTimeoutWatcher);
}
if ($error) {
$promisor->fail($error);
} else {
$promisor->succeed($result);
}
}

View File

@ -1,45 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<phpunit bootstrap="./test/bootstrap.php" colors="true">
<phpunit bootstrap="./vendor/autoload.php"> <testsuites>
<testsuite name="Tests">
<groups>
<exclude>
<group>SQL</group>
</exclude>
</groups>
<filter>
<blacklist>
<directory>./test</directory> <directory>./test</directory>
<directory>./vendor</directory> </testsuite>
</blacklist> </testsuites>
<whitelist> <filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory>./lib</directory> <directory>./lib</directory>
</whitelist> </whitelist>
</filter> </filter>
<testsuites>
<testsuite name="Addr Test Suite">
<directory suffix=".php">./test</directory>
</testsuite>
</testsuites>
<listeners>
<listener class="\Mockery\Adapter\Phpunit\TestListener"/>
</listeners>
<!-- <log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/> -->
<logging>
<!-- <log type="junit" target="./var/logfile.xml" logIncompleteSkipped="false"/> -->
<log type="coverage-text" target="php://stdout" showUncoveredFiles="false"/>
<!-- Enabling coverage makes the tests take three times as long, as well
as take a bit of time to generate the HTML pages. -->
<log type="coverage-html" target="./coverage/" charset="UTF-8"
highlight="false" lowUpperBound="35" highLowerBound="70"/>
</logging>
</phpunit> </phpunit>

View File

@ -1,102 +0,0 @@
<?php
namespace Amp\Dns\Test;
use Amp\NativeReactor;
use Amp\Dns\AddressModes;
use Amp\Dns\Client;
class ClientTest extends \PHPUnit_Framework_TestCase {
/**
* @expectedException \Amp\Dns\ResolutionException
*/
public function testFailToConnect() {
$reactor = new NativeReactor;
$client = new Client($reactor);
$client->setOption(Client::OP_SERVER_ADDRESS, '260.260.260.260');
$promise = $client->resolve('example.com', AddressModes::INET4_ADDR);
$result = \Amp\wait($promise, $reactor);
}
/**
* This is just for coverage - which is not worthwhile in itself,
* but it makes it easier to detect missing important coverage.
*/
public function testSetRequestTime() {
$reactor = new NativeReactor;
$client = new Client($reactor);
$client->setOption(Client::OP_MS_REQUEST_TIMEOUT, 1000);
$client->setOption(Client::OP_SERVER_PORT, 53);
}
/**
* @expectedException \DomainException
*/
public function testUnknownOptionThrowsException() {
$reactor = new NativeReactor;
$client = new Client($reactor);
$client->setOption('foo', 1000);
}
/**
* @expectedException \RuntimeException
* @group internet
*/
public function testSetAddressAfterConnectException() {
$reactor = new NativeReactor;
$client = new Client($reactor);
$promise = $client->resolve('google.com', AddressModes::INET4_ADDR);
$result = \Amp\wait($promise, $reactor);
$client->setOption(Client::OP_SERVER_ADDRESS, '260.260.260.260');
}
/**
* @expectedException \RuntimeException
* @group internet
*/
public function testSetPortAfterConnectException() {
$reactor = new NativeReactor;
$client = new Client($reactor);
$promise = $client->resolve('google.com', AddressModes::INET4_ADDR);
$result = \Amp\wait($promise, $reactor);
$client->setOption(Client::OP_SERVER_PORT, 53);
}
/**
* @expectedException \Amp\Dns\ResolutionException
*/
public function testNoAnswers() {
$reactor = new NativeReactor;
$client = new Client($reactor);
$promise = $client->resolve('googleaiusdisuhdihas.apsidjpasjdisjdajsoidaugiug.com', AddressModes::INET4_ADDR);
$result = \Amp\wait($promise, $reactor);
}
/**
* Test that the overflow of lookupIdCounter and requestIdCounter to
* zero occurs.
*/
public function testPendingIdOverflow() {
$reactor = new NativeReactor;
$class = new \ReflectionClass('Amp\Dns\Client');
$lookupIdProperty = $class->getProperty("lookupIdCounter");
$requestIdCounterProperty = $class->getProperty("requestIdCounter");
/** @var Client $client */
$client = $class->newInstance($reactor);
$lookupIdProperty->setAccessible(true);
$lookupIdProperty->setValue($client, PHP_INT_MAX);
$requestIdCounterProperty->setAccessible(true);
$requestIdCounterProperty->setValue($client, 65535);
$promise = $client->resolve('google.com', AddressModes::INET4_ADDR);
$result = \Amp\wait($promise, $reactor);
$lookupIdCounter = $lookupIdProperty->getValue($client);
$this->assertEquals(0, $lookupIdCounter);
$requestIdCounter = $lookupIdProperty->getValue($client);
$this->assertEquals(0, $requestIdCounter);
}
}

View File

@ -1,133 +0,0 @@
<?php
namespace Amp\Dns\Test;
use Amp\Dns\HostsFile;
use Amp\Dns\NameValidator;
use Amp\Dns\AddressModes as Mode;
/**
* Class HostsFileTest
* @group hosts
*/
class HostsFileTest extends \PHPUnit_Framework_TestCase {
public function testBasicIPV4() {
$tests = [
['192.168.1.1', 'host1.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR],
['192.168.1.2', 'host2.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR],
//Check that ipv4 is returned when both v4 and v6 are is set
['192.168.1.1', 'host1.example.com', Mode::INET4_ADDR | Mode::INET6_ADDR, Mode::INET4_ADDR],
['192.168.1.2', 'host2.example.com', Mode::INET4_ADDR | Mode::INET6_ADDR, Mode::INET4_ADDR],
//Check that ipv4 is returned when ANY_* is set
['192.168.1.1', 'host1.example.com', Mode::ANY_PREFER_INET4, Mode::INET4_ADDR],
['192.168.1.2', 'host2.example.com', Mode::ANY_PREFER_INET4, Mode::INET4_ADDR],
['192.168.1.1', 'host1.example.com', Mode::ANY_PREFER_INET6, Mode::INET4_ADDR],
['192.168.1.2', 'host2.example.com', Mode::ANY_PREFER_INET6, Mode::INET4_ADDR],
//Check request for ipv6 returns null
[null, 'host1.example.com', Mode::INET6_ADDR, null],
//Check non-existant domains return null
[null, 'host4.example.com', Mode::INET4_ADDR, null],
];
$this->runHostsFileTests($tests, __DIR__ . '/fixtures/ipv4Hosts.txt');
}
public function testBasicIPV6() {
$tests = [
//Examples taken from http://en.wikipedia.org/wiki/IPv6_address
['2001:db8::2:1', 'host1.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR],
['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR],
['2001:db8::1:0:0:1', 'host3.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR],
//Check that ipv6 is returned when both v4 and v6 are is set
['2001:db8::2:1', 'host1.example.com', Mode::INET6_ADDR | Mode::INET4_ADDR, Mode::INET6_ADDR],
['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::INET6_ADDR | Mode::INET4_ADDR, Mode::INET6_ADDR],
['2001:db8::1:0:0:1', 'host3.example.com', Mode::INET6_ADDR | Mode::INET4_ADDR, Mode::INET6_ADDR],
//Check that ipv6 is returned when ANY_* is set
['2001:db8::2:1', 'host1.example.com', Mode::ANY_PREFER_INET4, Mode::INET6_ADDR],
['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::ANY_PREFER_INET4, Mode::INET6_ADDR],
['2001:db8::1:0:0:1', 'host3.example.com', Mode::ANY_PREFER_INET4, Mode::INET6_ADDR],
['2001:db8::2:1', 'host1.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR],
['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR],
['2001:db8::1:0:0:1', 'host3.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR],
//Check request for ipv4 returns null
[null, 'host1.example.com', Mode::INET4_ADDR, null],
//Check non-existant domains return null
[null, 'host4.example.com', Mode::INET6_ADDR, null],
];
$this->runHostsFileTests($tests, __DIR__ . '/fixtures/ipv6Hosts.txt');
}
public function testMixedIPVersions() {
$tests = [
//Examples taken from http://en.wikipedia.org/wiki/IPv6_address
['2001:db8::2:1', 'host1.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR],
['2001:db8:0:1:1:1:1:1', 'host2.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR],
['2001:db8::1:0:0:1', 'host3.example.com', Mode::INET6_ADDR, Mode::INET6_ADDR],
['192.168.1.1', 'host1.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR],
['192.168.1.2', 'host2.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR],
['192.168.1.4', 'host4.example.com', Mode::INET4_ADDR, Mode::INET4_ADDR],
//Check that v4 is returned by default
['192.168.1.1', 'host1.example.com', Mode::INET4_ADDR | Mode::INET6_ADDR, Mode::INET4_ADDR],
//Check that the prefer inet6 works
['2001:db8::2:1', 'host1.example.com', Mode::INET4_ADDR | Mode::INET6_ADDR | Mode::PREFER_INET6, Mode::INET6_ADDR],
//Check that the ANY_* works
['192.168.1.1', 'host1.example.com', Mode::ANY_PREFER_INET4, Mode::INET4_ADDR],
['2001:db8::2:1', 'host1.example.com', Mode::ANY_PREFER_INET6, Mode::INET6_ADDR],
//Check that a host that is only listed as ipv4 does not return a result for ipv6
[null, 'host4.example.com', Mode::INET6_ADDR, null],
//Check that a host that is only listed as ipv6 does not return a result for ipv4
[null, 'host3.example.com', Mode::INET4_ADDR, null],
];
$this->runHostsFileTests($tests, __DIR__ . '/fixtures/mixedVersionHosts.txt');
}
public function runHostsFileTests($tests, $hostsFile) {
$nameValidator = new NameValidator;
$hostsFile = new HostsFile($nameValidator, $hostsFile);
foreach ($tests as $i => $test) {
list($expectedResult, $hostname, $inputAddrMode, $expectedAddrMode) = $test;
$result = $hostsFile->resolve($hostname, $inputAddrMode);
if ($expectedResult === null) {
$this->assertNull($result);
} else {
list($resolvedAddr, $resolvedMode) = $result;
$this->assertEquals(
$expectedAddrMode,
$resolvedMode,
"Failed to resolve $hostname to $expectedResult. " .
"Expected `" . var_export($expectedAddrMode, true) . "` " .
"but got `" . var_export($resolvedAddr, true) . "` " .
" when running test #" . $i
);
$this->assertEquals(
$expectedResult,
$resolvedAddr,
"Failed to resolve $hostname to $expectedResult. " .
"Expected `$expectedResult` but got `$resolvedAddr` " .
" when running test #" . $i
);
}
}
}
}

View File

@ -2,170 +2,46 @@
namespace Amp\Dns\Test; namespace Amp\Dns\Test;
use Amp\NativeReactor;
use Amp\Dns\Cache;
use Amp\Dns\Client;
use Amp\Dns\Resolver;
use Amp\Dns\Cache\APCCache;
use Amp\Dns\Cache\MemoryCache;
use Amp\Dns\Cache\RedisCache;
use Predis\Client as RedisClient;
use Predis\Connection\ConnectionException as RedisConnectionException;
class IntegrationTest extends \PHPUnit_Framework_TestCase { class IntegrationTest extends \PHPUnit_Framework_TestCase {
private static $redisEnabled = true; protected function setUp() {
private static $redisParameters = [ \Amp\reactor(\Amp\driver());
'connection_timeout' => 2,
'read_write_timeout' => 2,
];
public static function setUpBeforeClass() {
try {
$predisClient = new RedisClient(self::$redisParameters, []);
$predisClient->ping();
//It's connected
} catch (RedisConnectionException $rce) {
self::$redisEnabled = false;
}
}
public function testWithNullCache() {
$this->basicRun(null);
}
public function testWithMemoryCache() {
$memoryCache = new MemoryCache();
$this->basicRun($memoryCache);
}
/**
* @requires extension APC
*/
public function testWithApcCache() {
$prefix = time().uniqid('CacheTest');
$apcCache = new APCCache($prefix);
$this->basicRun($apcCache);
}
public function testWithRedisCache() {
if (self::$redisEnabled != true) {
$this->markTestSkipped("Could not connect to Redis, skipping test.");
return;
}
$prefix = time().'_'.uniqid('CacheTest');
try {
$redisClient = new RedisClient(self::$redisParameters, []);
}
catch (RedisConnectionException $rce) {
$this->markTestIncomplete("Could not connect to Redis server, cannot test redis cache.");
return;
}
$redisCache = new RedisCache($redisClient, $prefix);
$this->basicRun($redisCache);
} }
/** /**
* @group internet * @group internet
*/ */
public function basicRun(Cache $cache = null) { public function testResolve() {
\Amp\run(function () {
$names = [ $names = [
'google.com', "google.com",
'github.com', "github.com",
'stackoverflow.com', "stackoverflow.com",
'localhost', "localhost",
'192.168.0.1', "192.168.0.1",
'::1', "::1",
]; ];
$reactor = new NativeReactor;
$client = new Client($reactor, null, null, $cache);
$resolver = new Resolver($client);
$promises = [];
foreach ($names as $name) { foreach ($names as $name) {
$promises[$name] = $resolver->resolve($name); list($addr, $mode) = (yield \Amp\Dns\resolve($name));
} $inAddr = @\inet_pton($addr);
$comboPromise = \Amp\all($promises);
$results = \Amp\wait($comboPromise, $reactor);
foreach ($results as $name => $addrStruct) {
list($addr, $type) = $addrStruct;
$validIP = @inet_pton($addr);
$this->assertNotFalse( $this->assertNotFalse(
$validIP, $inAddr,
"Server name $name did not resolve to a valid IP address" "Server name $name did not resolve to a valid IP address"
); );
if (isset($inAddr[15])) {
$this->assertSame(
\Amp\Dns\MODE_INET6,
$mode,
"Returned mode parameter did not match expected MODE_INET6"
);
} else {
$this->assertSame(
\Amp\Dns\MODE_INET4,
$mode,
"Returned mode parameter did not match expected MODE_INET4"
);
} }
} }
});
/**
* Check that caches do actually cache results.
*/
function testCachingOfResults() {
$memoryCache = new MemoryCache();
$namesFirstRun = [
'google.com',
'github.com',
'google.com',
'github.com',
];
$namesSecondRun = [
'google.com',
'github.com',
];
$setCount = count(array_unique(array_merge($namesFirstRun, $namesSecondRun)));
$getCount = count($namesFirstRun) + count($namesSecondRun);
$mockedCache = \Mockery::mock($memoryCache);
/** @var $mockedCache \Mockery\Mock */
$mockedCache->shouldReceive('store')->times($setCount)->passthru();
$mockedCache->shouldReceive('get')->times($getCount)->passthru();
$mockedCache->makePartial();
$reactor = new NativeReactor;
$client = new Client($reactor, null, null, $mockedCache);
$resolver = new Resolver($client);
$promises = [];
foreach ($namesFirstRun as $name) {
$promises[$name] = $resolver->resolve($name);
}
$comboPromise = \Amp\all($promises);
$results = \Amp\wait($comboPromise, $reactor);
foreach ($results as $name => $addrStruct) {
list($addr, $type) = $addrStruct;
$validIP = @inet_pton($addr);
$this->assertNotFalse(
$validIP,
"Server name $name did not resolve to a valid IP address"
);
}
$promises = [];
foreach ($namesSecondRun as $name) {
$promises[$name] = $resolver->resolve($name);
}
$comboPromise = \Amp\all($promises);
$results = \Amp\wait($comboPromise, $reactor);
foreach ($results as $name => $addrStruct) {
list($addr, $type) = $addrStruct;
$validIP = @inet_pton($addr);
$this->assertNotFalse(
$validIP,
"Server name $name did not resolve to a valid IP address"
);
}
} }
} }

View File

@ -1,80 +0,0 @@
<?php
namespace Amp\Dns\Test;
use Amp\NativeReactor;
use Amp\Dns\AddressModes;
use Amp\Dns\Client;
use Amp\Dns\HostsFile;
use Amp\Dns\Resolver;
use Amp\Dns\ResolutionErrors;
class ResolverTest extends \PHPUnit_Framework_TestCase {
private function createResolver($hostsFile = null) {
$reactor = new NativeReactor;
$hostsFile = new HostsFile(null, $hostsFile);
$client = new Client($reactor);
$resolver = new Resolver($client, $hostsFile);
return [$reactor, $resolver];
}
/**
* @expectedException Amp\Dns\ResolutionException
* @expectedErrorCode Amp\Dns\ResolutionErrors::ERR_INVALID_NAME
*/
public function testInvalidName() {
list($reactor, $resolver) = $this->createResolver();
$alphabet = implode(range('a', 'z'));
$tooLongName = $alphabet.$alphabet; //52
$tooLongName = $tooLongName.$tooLongName; //104
$tooLongName = $tooLongName.$tooLongName; //208
$tooLongName = $tooLongName.$alphabet; //234
$tooLongName = $tooLongName.$alphabet; //260
$promise = $resolver->resolve($tooLongName, AddressModes::PREFER_INET6);
$addrStruct = \Amp\wait($promise, $reactor);
}
/**
* @group internet
* @expectedException Amp\Dns\ResolutionException
* @expectedErrorCode Amp\Dns\ResolutionErrors::ERR_NO_RECORD
*/
public function testUnknownName() {
list($reactor, $resolver) = $this->createResolver();
$promise = $resolver->resolve("doesntexist", AddressModes::PREFER_INET6);
$addrStruct = \Amp\wait($promise, $reactor);
}
public function testLocalHostResolution() {
list($reactor, $resolver) = $this->createResolver();
$promise = $resolver->resolve("localhost", AddressModes::INET4_ADDR);
list($addr, $type) = \Amp\wait($promise, $reactor);
$this->assertSame('127.0.0.1', $addr);
$this->assertSame(AddressModes::INET4_ADDR, $type, "Wrong result type - should be INET4_ADDR but got $type");
$promise = $resolver->resolve("localhost", AddressModes::PREFER_INET6);
list($addr, $type) = \Amp\wait($promise, $reactor);
$this->assertSame('::1', $addr);
$this->assertSame(AddressModes::INET6_ADDR, $type, "Wrong result type - should be INET6_ADDR but got $type");
}
public function testHostsFileResolution() {
$hostsFile = __DIR__ . '/fixtures/resolverTest.txt';
list($reactor, $resolver) = $this->createResolver($hostsFile);
$promise = $resolver->resolve("host1.example.com", AddressModes::INET4_ADDR);
list($addr, $type) = \Amp\wait($promise, $reactor);
$this->assertSame('192.168.1.1', $addr);
$this->assertSame(AddressModes::INET4_ADDR, $type);
$promise = $resolver->resolve("resolvertest", AddressModes::INET4_ADDR);
list($addr, $type) = \Amp\wait($promise, $reactor);
$this->assertSame('192.168.1.3', $addr);
$this->assertSame(AddressModes::INET4_ADDR, $type);
}
}

View File

@ -1,300 +0,0 @@
<?php
namespace Amp\Dns\Test;
use Amp\Dns\AddressModes;
use Amp\Dns\ResponseInterpreter;
use LibDNS\Decoder\DecoderFactory;
class ResponseInterpreterTest extends \PHPUnit_Framework_TestCase {
//Example packets below are taken from http://wiki.wireshark.org/SampleCaptures:
//"Standard query response","DNS","70","domain","32795","6"
static private $standardQueryResponse = [
0x49, 0xa1, 0x81, 0x80, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x06, 0x67, 0x6f, 0x6f,
0x67, 0x6c, 0x65, 0x03, 0x63, 0x6f, 0x6d, 0x00,
0x00, 0x1d, 0x00, 0x01
];
//"Standard query response PTR 66-192-9-104.gen.twtelecom.net","DNS","129","domain","32795","8"
static private $standardQueryResponsePTR = [
0x9b, 0xbb, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x03, 0x31, 0x30, 0x34,
0x01, 0x39, 0x03, 0x31, 0x39, 0x32, 0x02, 0x36,
0x36, 0x07, 0x69, 0x6e, 0x2d, 0x61, 0x64, 0x64,
0x72, 0x04, 0x61, 0x72, 0x70, 0x61, 0x00, 0x00,
0x0c, 0x00, 0x01, 0xc0, 0x0c, 0x00, 0x0c, 0x00,
0x01, 0x00, 0x01, 0x51, 0x25, 0x00, 0x20, 0x0c,
0x36, 0x36, 0x2d, 0x31, 0x39, 0x32, 0x2d, 0x39,
0x2d, 0x31, 0x30, 0x34, 0x03, 0x67, 0x65, 0x6e,
0x09, 0x74, 0x77, 0x74, 0x65, 0x6c, 0x65, 0x63,
0x6f, 0x6d, 0x03, 0x6e, 0x65, 0x74, 0x00
];
//"Standard query response A 204.152.190.12","DNS","90","domain","32795","10"
static private $standardQueryResponseA = [
0x75, 0xc0, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77,
0x06, 0x6e, 0x65, 0x74, 0x62, 0x73, 0x64, 0x03,
0x6f, 0x72, 0x67, 0x00, 0x00, 0x01, 0x00, 0x01,
0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01,
0x40, 0xef, 0x00, 0x04, 0xcc, 0x98, 0xbe, 0x0c
];
//"Standard query response AAAA 2001:4f8:4:7:2e0:81ff:fe52:9a6b","DNS","102","domain","32795","14"
static private $standardQueryResponseIPV6 = [
0x7f, 0x39, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77,
0x06, 0x6e, 0x65, 0x74, 0x62, 0x73, 0x64, 0x03,
0x6f, 0x72, 0x67, 0x00, 0x00, 0x1c, 0x00, 0x01,
0xc0, 0x0c, 0x00, 0x1c, 0x00, 0x01, 0x00, 0x01,
0x51, 0x44, 0x00, 0x10, 0x20, 0x01, 0x04, 0xf8,
0x00, 0x04, 0x00, 0x07, 0x02, 0xe0, 0x81, 0xff,
0xfe, 0x52, 0x9a, 0x6b
];
//"Standard query response CNAME www.l.google.com","DNS","94","domain","32795","16"
static private $standardQueryResponseCNAME = [
0x8d, 0xb3, 0x81, 0x80, 0x00, 0x01, 0x00, 0x01,
0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77,
0x06, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x03,
0x63, 0x6f, 0x6d, 0x00, 0x00, 0x1c, 0x00, 0x01,
0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00,
0x02, 0x79, 0x00, 0x08, 0x03, 0x77, 0x77, 0x77,
0x01, 0x6c, 0xc0, 0x10
];
//Standard query response, No such name DNS 79 domain 32795 22
static private $standardQueryResponseNoSuchName = [
0x26, 0x6d, 0x85, 0x83, 0x00, 0x01, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77,
0x07, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65,
0x07, 0x6e, 0x6f, 0x74, 0x67, 0x69, 0x6e, 0x68,
0x00, 0x00, 0x1c, 0x00, 0x01
];
//"Standard query response AAAA 2001:4f8:0:2::d A 204.152.184.88","DNS","115","domain","32795","24
static private $standardQueryResponseMixed = [
0xfe, 0xe3, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77,
0x03, 0x69, 0x73, 0x63, 0x03, 0x6f, 0x72, 0x67,
0x00, 0x00, 0xff, 0x00, 0x01, 0xc0, 0x0c, 0x00,
0x1c, 0x00, 0x01, 0x00, 0x00, 0x02, 0x58, 0x00,
0x10, 0x20, 0x01, 0x04, 0xf8, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0d, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0x02, 0x58, 0x00, 0x04, 0xcc, 0x98, 0xb8,
0x58
];
//Standard query response AAAA 2001:4f8:0:2::d A 204.152.184.88 DNS 115 domain 32795 24
static private $multipleResponse = [
0xfe, 0xe3, 0x81, 0x80, 0x00, 0x01, 0x00, 0x02,
0x00, 0x00, 0x00, 0x00, 0x03, 0x77, 0x77, 0x77,
0x03, 0x69, 0x73, 0x63, 0x03, 0x6f, 0x72, 0x67,
0x00, 0x00, 0xff, 0x00, 0x01, 0xc0, 0x0c, 0x00,
0x1c, 0x00, 0x01, 0x00, 0x00, 0x02, 0x58, 0x00,
0x10, 0x20, 0x01, 0x04, 0xf8, 0x00, 0x00, 0x00,
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x0d, 0xc0, 0x0c, 0x00, 0x01, 0x00, 0x01, 0x00,
0x00, 0x02, 0x58, 0x00, 0x04, 0xcc, 0x98, 0xb8,
0x58
];
static private $bbcNews = [
//This should be equivalent to
// ;; ANSWER SECTION:
// news.bbc.co.uk. 655 IN CNAME newswww.bbc.net.uk.
// newswww.bbc.net.uk. 174 IN A 212.58.246.82
// newswww.bbc.net.uk. 174 IN A 212.58.246.83
0xe1, 0x16, 0x81, 0x80, 0x00, 0x01, 0x00, 0x03,
0x00, 0x00, 0x00, 0x00, 0x04, 0x6e, 0x65, 0x77,
0x73, 0x03, 0x62, 0x62, 0x63, 0x02, 0x63, 0x6f,
0x02, 0x75, 0x6b, 0x00, 0x00, 0x01, 0x00, 0x01,
0xc0, 0x0c, 0x00, 0x05, 0x00, 0x01, 0x00, 0x00,
0x02, 0x8f, 0x00, 0x12, 0x07, 0x6e, 0x65, 0x77,
0x73, 0x77, 0x77, 0x77, 0x03, 0x62, 0x62, 0x63,
0x03, 0x6e, 0x65, 0x74, 0xc0, 0x18, 0xc0, 0x2c,
0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xae,
0x00, 0x04, 0xd4, 0x3a, 0xf6, 0x52, 0xc0, 0x2c,
0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0xae,
0x00, 0x04, 0xd4, 0x3a, 0xf6, 0x53
];
public function getPacketString(array $bytes) {
$packet = '';
foreach ($bytes as $byte) {
$packet .= chr($byte);
}
return $packet;
}
public function testCatchesExceptionAndReturnsNull() {
$decoder = \Mockery::mock('LibDNS\Decoder\Decoder');
$decoder->shouldReceive('decode')->withAnyArgs()->andThrow("Exception", "Testing bad packet");
$responseInterpreter = new ResponseInterpreter($decoder);
$result = $responseInterpreter->decode("SomePacket");
$this->assertNull($result);
}
public function testInvalidMessage() {
$message = \Mockery::mock('LibDNS\Messages\Message');
$message->shouldReceive('getType')->once()->andReturn(\LibDNS\Messages\MessageTypes::QUERY);
$decoder = \Mockery::mock('LibDNS\Decoder\Decoder');
$decoder->shouldReceive('decode')->once()->andReturn($message);
$responseInterpreter = new ResponseInterpreter($decoder);
$result = $responseInterpreter->decode("SomePacket");
$this->assertNull($result);
}
public function testInvalidResponseCode() {
$message = \Mockery::mock('LibDNS\Messages\Message');
$message->shouldReceive('getType')->once()->andReturn(\LibDNS\Messages\MessageTypes::RESPONSE);
$message->shouldReceive('getResponseCode')->once()->andReturn(42);
$decoder = \Mockery::mock('LibDNS\Decoder\Decoder');
$decoder->shouldReceive('decode')->once()->andReturn($message);
$responseInterpreter = new ResponseInterpreter($decoder);
$result = $responseInterpreter->decode("SomePacket");
$this->assertNull($result);
}
/**
* @group CNAME
*/
public function testNewsBBC() {
$testPacket = $this->getPacketString(self::$bbcNews);
$decoder = (new DecoderFactory)->create();
$responseInterpreter = new ResponseInterpreter($decoder);
$decoded = $responseInterpreter->decode($testPacket);
list($id, $response) = $decoded;
//Check the IPV4 result
$interpreted = $responseInterpreter->interpret($response, AddressModes::INET4_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::INET4_ADDR, $type);
//@TODO - this should be multiple - 212.58.246.82 and 212.58.246.83
$this->assertSame("212.58.246.82", $addr);
$this->assertSame(174, $ttl);
$interpreted = $responseInterpreter->interpret($response, AddressModes::INET6_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals("newswww.bbc.net.uk", $addr);
$this->assertEquals(AddressModes::CNAME, $type);
$this->assertNull($ttl);
}
public function createResponseInterpreter() {
$decoder = (new DecoderFactory)->create();
$responseInterpreter = new ResponseInterpreter($decoder);
return $responseInterpreter;
}
public function testNoResults() {
$responseInterpreter = $this->createResponseInterpreter();
$packet = $this->getPacketString(self::$standardQueryResponse);
$decoded = $responseInterpreter->decode($packet);
list($id, $message) = $decoded;
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR);
$this->assertNull($interpreted);
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR);
$this->assertNull($interpreted);
}
public function testNoSuchName() {
$responseInterpreter = $this->createResponseInterpreter();
$packet = $this->getPacketString(self::$standardQueryResponseNoSuchName);
$decoded = $responseInterpreter->decode($packet);
list($id, $message) = $decoded;
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR);
$this->assertNull($interpreted, "Response with 'no such name' was not interpreted to null.");
}
public function testMixed() {
$responseInterpreter = $this->createResponseInterpreter();
$packet = $this->getPacketString(self::$standardQueryResponseMixed);
$decoded = $responseInterpreter->decode($packet);
list($id, $message) = $decoded;
//Get the IPv4 part
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::INET4_ADDR, $type);
$this->assertEquals('204.152.184.88', $addr);
$this->assertEquals(600, $ttl);
//Get the IPv6 part
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::INET6_ADDR, $type);
$this->assertEquals('2001:4f8:0:2::d', $addr);
$this->assertEquals(600, $ttl);
}
public function testIPv4() {
$responseInterpreter = $this->createResponseInterpreter();
$packet = $this->getPacketString(self::$standardQueryResponseA);
$decoded = $responseInterpreter->decode($packet);
list($id, $message) = $decoded;
//Get the IPv4 part
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::INET4_ADDR, $type);
$this->assertEquals('204.152.190.12', $addr);
$this->assertEquals(82159, $ttl);
//Get the IPv6 part
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR);
$this->assertNull($interpreted);
}
public function testIPv6() {
$responseInterpreter = $this->createResponseInterpreter();
$packet = $this->getPacketString(self::$standardQueryResponseIPV6);
$decoded = $responseInterpreter->decode($packet);
list($id, $message) = $decoded;
//Get the IPv4 part
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR);
$this->assertNull($interpreted);
//Get the IPv6 part
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::INET6_ADDR, $type);
$this->assertEquals('2001:4f8:4:7:2e0:81ff:fe52:9a6b', $addr);
$this->assertEquals(86340, $ttl);
}
public function testCnameResponse() {
$responseInterpreter = $this->createResponseInterpreter();
$packet = $this->getPacketString(self::$standardQueryResponseCNAME);
$decoded = $responseInterpreter->decode($packet);
list($id, $message) = $decoded;
//Try to get an IPv4 answer - but actually get a CNAME
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET4_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::CNAME, $type);
$this->assertEquals('www.l.google.com', $addr);
//Try to get an IPv6 answer - but actually get a CNAME
$interpreted = $responseInterpreter->interpret($message, AddressModes::INET6_ADDR);
list($type, $addr, $ttl) = $interpreted;
$this->assertEquals(AddressModes::CNAME, $type);
$this->assertEquals('www.l.google.com', $addr);
}
}

View File

@ -2,8 +2,8 @@
error_reporting(E_ALL); error_reporting(E_ALL);
if (ini_get('opcache.enable') == true && if (ini_get("opcache.enable") == true &&
ini_get('opcache.save_comments') == false) { ini_get("opcache.save_comments") == false) {
echo "Cannot run tests. OPCache is enabled and is stripping comments, which are required by PHPUnit to provide data for the tests.\n"; echo "Cannot run tests. OPCache is enabled and is stripping comments, which are required by PHPUnit to provide data for the tests.\n";
exit(-1); exit(-1);
} }

View File

@ -1,2 +0,0 @@
192.168.1.1 host1.example.com
192.168.1.2 host2.example.com

View File

@ -1,3 +0,0 @@
2001:db8::2:1 host1.example.com
2001:db8:0:1:1:1:1:1 host2.example.com
2001:db8::1:0:0:1 host3.example.com

View File

@ -1,14 +0,0 @@
192.168.1.1 host1.example.com
192.168.1.2 host2.example.com
192.168.1.4 host4.example.com
2001:db8::2:1 host1.example.com
2001:db8:0:1:1:1:1:1 host2.example.com
2001:db8::1:0:0:1 host3.example.com
#next line is empty for coverage
//empty_line_for_coverage
#next line has invalid address for coverage
1.2.300 doesntexist.com

View File

@ -1,8 +0,0 @@
192.168.1.1 host1.example.com
192.168.1.2 host2.example.com
192.168.1.3 resolvertest
2001:db8::2:1 host1.example.com
2001:db8:0:1:1:1:1:1 host2.example.com
2001:db8::1:0:0:1 host3.example.com