pax_global_header00006660000000000000000000000064133720102510014504gustar00rootroot0000000000000052 comment=0a0bedfec72b38406413c6ea01e1c015bd0bf72b dns-0.4.16/000077500000000000000000000000001337201025100123605ustar00rootroot00000000000000dns-0.4.16/.gitignore000066400000000000000000000000241337201025100143440ustar00rootroot00000000000000composer.lock vendordns-0.4.16/.travis.yml000066400000000000000000000006431337201025100144740ustar00rootroot00000000000000language: php php: # - 5.3 # requires old distro, see below - 5.4 - 5.5 - 5.6 - 7.0 - 7.1 - 7.2 - hhvm # ignore errors, see below # lock distro so new future defaults will not break the build dist: trusty matrix: include: - php: 5.3 dist: precise allow_failures: - php: hhvm sudo: false install: - composer install --no-interaction script: - vendor/bin/phpunit --coverage-text dns-0.4.16/CHANGELOG.md000066400000000000000000000143171337201025100141770ustar00rootroot00000000000000# Changelog ## 0.4.16 (2018-11-11) * Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references. (#118 by @clue) * Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages, malformed record data or malformed compressed domain name labels. (#115 and #117 by @clue) * Fix: Fix interpretation of TTL as UINT32 with most significant bit unset. (#116 by @clue) * Fix: Fix caching advanced MX/SRV/TXT/SOA structures. (#112 by @clue) ## 0.4.15 (2018-07-02) * Feature: Add `resolveAll()` method to support custom query types in `Resolver`. (#110 by @clue and @WyriHaximus) ```php $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; }); ``` * Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records. (#104, #105, #106, #107 and #108 by @clue) * Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data. (#104 by @clue) * Feature: Improve error messages for failed queries and improve documentation. (#109 by @clue) * Feature: Add reverse DNS lookup example. (#111 by @clue) ## 0.4.14 (2018-06-26) * Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages to avoid cache poisoning attacks and deprecate legacy `Executor`. (#101 and #103 by @clue) * Feature: Forward compatibility with Cache 0.5 (#102 by @clue) * Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API. (#99 by @clue) ## 0.4.13 (2018-02-27) * Add `Config::loadSystemConfigBlocking()` to load default system config and support parsing DNS config on all supported platforms (`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows) (#92, #93, #94 and #95 by @clue) ```php $config = Config::loadSystemConfigBlocking(); $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; ``` * Remove unneeded cyclic dependency on react/socket (#96 by @clue) ## 0.4.12 (2018-01-14) * Improve test suite by adding forward compatibility with PHPUnit 6, test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases, add test group to skip integration tests relying on internet connection and add minor documentation improvements. (#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor) ## 0.4.11 (2017-08-25) * Feature: Support resolving from default hosts file (#75, #76 and #77 by @clue) This means that resolving hosts such as `localhost` will now work as expected across all platforms with no changes required: ```php $resolver->resolve('localhost')->then(function ($ip) { echo 'IP: ' . $ip; }); ``` The new `HostsExecutor` exists for advanced usage and is otherwise used internally for this feature. ## 0.4.10 (2017-08-10) * Feature: Forward compatibility with EventLoop v1.0 and v0.5 and lock minimum dependencies and work around circular dependency for tests (#70 and #71 by @clue) * Fix: Work around DNS timeout issues for Windows users (#74 by @clue) * Documentation and examples for advanced usage (#66 by @WyriHaximus) * Remove broken TCP code, do not retry with invalid TCP query (#73 by @clue) * Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and lock Travis distro so new defaults will not break the build and fix failing tests for PHP 7.1 (#68 by @WyriHaximus and #69 and #72 by @clue) ## 0.4.9 (2017-05-01) * Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 (#61 by @clue) ## 0.4.8 (2017-04-16) * Feature: Add support for the AAAA record type to the protocol parser (#58 by @othillo) * Feature: Add support for the PTR record type to the protocol parser (#59 by @othillo) ## 0.4.7 (2017-03-31) * Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component (#57 by @clue) ## 0.4.6 (2017-03-11) * Fix: Fix DNS timeout issues for Windows users and add forward compatibility with Stream v0.5 and upcoming v0.6 (#53 by @clue) * Improve test suite by adding PHPUnit to `require-dev` (#54 by @clue) ## 0.4.5 (2017-03-02) * Fix: Ensure we ignore the case of the answer (#51 by @WyriHaximus) * Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal code re-use for upcoming versions. (#48 and #49 by @clue) ## 0.4.4 (2017-02-13) * Fix: Fix handling connection and stream errors (#45 by @clue) * Feature: Add examples and forward compatibility with upcoming Socket v0.5 component (#46 and #47 by @clue) ## 0.4.3 (2016-07-31) * Feature: Allow for cache adapter injection (#38 by @WyriHaximus) ```php $factory = new React\Dns\Resolver\Factory(); $cache = new MyCustomCacheInstance(); $resolver = $factory->createCached('8.8.8.8', $loop, $cache); ``` * Feature: Support Promise cancellation (#35 by @clue) ```php $promise = $resolver->resolve('reactphp.org'); $promise->cancel(); ``` ## 0.4.2 (2016-02-24) * Repository maintenance, split off from main repo, improve test suite and documentation * First class support for PHP7 and HHVM (#34 by @clue) * Adjust compatibility to 5.3 (#30 by @clue) ## 0.4.1 (2014-04-13) * Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus) ## 0.4.0 (2014-02-02) * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks * BC break: Update to React/Promise 2.0 * Bug fix: Properly resolve CNAME aliases * Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 * Bump React dependencies to v0.4 ## 0.3.2 (2013-05-10) * Feature: Support default port for IPv6 addresses (@clue) ## 0.3.0 (2013-04-14) * Bump React dependencies to v0.3 ## 0.2.6 (2012-12-26) * Feature: New cache component, used by DNS ## 0.2.5 (2012-11-26) * Version bump ## 0.2.4 (2012-11-18) * Feature: Change to promise-based API (@jsor) ## 0.2.3 (2012-11-14) * Version bump ## 0.2.2 (2012-10-28) * Feature: DNS executor timeout handling (@arnaud-lb) * Feature: DNS retry executor (@arnaud-lb) ## 0.2.1 (2012-10-14) * Minor adjustments to DNS parser ## 0.2.0 (2012-09-10) * Feature: DNS resolver dns-0.4.16/LICENSE000066400000000000000000000020551337201025100133670ustar00rootroot00000000000000Copyright (c) 2012 Igor Wiedler, Chris Boden 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. dns-0.4.16/README.md000066400000000000000000000236321337201025100136450ustar00rootroot00000000000000# Dns [![Build Status](https://travis-ci.org/reactphp/dns.svg?branch=master)](https://travis-ci.org/reactphp/dns) Async DNS resolver for [ReactPHP](https://reactphp.org/). The main point of the DNS component is to provide async DNS resolution. However, it is really a toolkit for working with DNS messages, and could easily be used to create a DNS server. **Table of contents** * [Basic usage](#basic-usage) * [Caching](#caching) * [Custom cache adapter](#custom-cache-adapter) * [Resolver](#resolver) * [resolve()](#resolve) * [resolveAll()](#resolveall) * [Advanced usage](#advanced-usage) * [UdpTransportExecutor](#udptransportexecutor) * [HostsFileExecutor](#hostsfileexecutor) * [Install](#install) * [Tests](#tests) * [License](#license) * [References](#references) ## Basic usage The most basic usage is to just create a resolver through the resolver factory. All you need to give it is a nameserver, then you can start resolving names, baby! ```php $loop = React\EventLoop\Factory::create(); $config = React\Dns\Config\Config::loadSystemConfigBlocking(); $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new React\Dns\Resolver\Factory(); $dns = $factory->create($server, $loop); $dns->resolve('igor.io')->then(function ($ip) { echo "Host: $ip\n"; }); $loop->run(); ``` See also the [first example](examples). The `Config` class can be used to load the system default config. This is an operation that may access the filesystem and block. Ideally, this method should thus be executed only once before the loop starts and not repeatedly while it is running. Note that this class may return an *empty* configuration if the system config can not be loaded. As such, you'll likely want to apply a default nameserver as above if none can be found. > Note that the factory loads the hosts file from the filesystem once when creating the resolver instance. Ideally, this method should thus be executed only once before the loop starts and not repeatedly while it is running. But there's more. ## Caching You can cache results by configuring the resolver to use a `CachedExecutor`: ```php $loop = React\EventLoop\Factory::create(); $config = React\Dns\Config\Config::loadSystemConfigBlocking(); $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new React\Dns\Resolver\Factory(); $dns = $factory->createCached($server, $loop); $dns->resolve('igor.io')->then(function ($ip) { echo "Host: $ip\n"; }); ... $dns->resolve('igor.io')->then(function ($ip) { echo "Host: $ip\n"; }); $loop->run(); ``` If the first call returns before the second, only one query will be executed. The second result will be served from an in memory cache. This is particularly useful for long running scripts where the same hostnames have to be looked up multiple times. See also the [third example](examples). ### Custom cache adapter By default, the above will use an in memory cache. You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead: ```php $cache = new React\Cache\ArrayCache(); $loop = React\EventLoop\Factory::create(); $factory = new React\Dns\Resolver\Factory(); $dns = $factory->createCached('8.8.8.8', $loop, $cache); ``` See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). ## Resolver ### resolve() The `resolve(string $domain): PromiseInterface` method can be used to resolve the given $domain name to a single IPv4 address (type `A` query). ```php $resolver->resolve('reactphp.org')->then(function ($ip) { echo 'IP for reactphp.org is ' . $ip . PHP_EOL; }); ``` This is one of the main methods in this package. It sends a DNS query for the given $domain name to your DNS server and returns a single IP address on success. If the DNS server sends a DNS response message that contains more than one IP address for this query, it will randomly pick one of the IP addresses from the response. If you want the full list of IP addresses or want to send a different type of query, you should use the [`resolveAll()`](#resolveall) method instead. If the DNS server sends a DNS response message that indicates an error code, this method will reject with a `RecordNotFoundException`. Its message and code can be used to check for the response code. If the DNS communication fails and the server does not respond with a valid response message, this message will reject with an `Exception`. Pending DNS queries can be cancelled by cancelling its pending promise like so: ```php $promise = $resolver->resolve('reactphp.org'); $promise->cancel(); ``` ### resolveAll() The `resolveAll(string $host, int $type): PromiseInterface` method can be used to resolve all record values for the given $domain name and query $type. ```php $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; }); $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; }); ``` This is one of the main methods in this package. It sends a DNS query for the given $domain name to your DNS server and returns a list with all record values on success. If the DNS server sends a DNS response message that contains one or more records for this query, it will return a list with all record values from the response. You can use the `Message::TYPE_*` constants to control which type of query will be sent. Note that this method always returns a list of record values, but each record value type depends on the query type. For example, it returns the IPv4 addresses for type `A` queries, the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, `CNAME` and `PTR` queries and structured data for other queries. See also the `Record` documentation for more details. If the DNS server sends a DNS response message that indicates an error code, this method will reject with a `RecordNotFoundException`. Its message and code can be used to check for the response code. If the DNS communication fails and the server does not respond with a valid response message, this message will reject with an `Exception`. Pending DNS queries can be cancelled by cancelling its pending promise like so: ```php $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); $promise->cancel(); ``` ## Advanced Usage ### UdpTransportExecutor The `UdpTransportExecutor` can be used to send DNS queries over a UDP transport. This is the main class that sends a DNS query to your DNS server and is used internally by the `Resolver` for the actual message transport. For more advanced usages one can utilize this class directly. The following example looks up the `IPv6` address for `igor.io`. ```php $loop = Factory::create(); $executor = new UdpTransportExecutor($loop); $executor->query( '8.8.8.8:53', new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) )->then(function (Message $message) { foreach ($message->answers as $answer) { echo 'IPv6: ' . $answer->data . PHP_EOL; } }, 'printf'); $loop->run(); ``` See also the [fourth example](examples). Note that this executor does not implement a timeout, so you will very likely want to use this in combination with a `TimeoutExecutor` like this: ```php $executor = new TimeoutExecutor( new UdpTransportExecutor($loop), 3.0, $loop ); ``` Also note that this executor uses an unreliable UDP transport and that it does not implement any retry logic, so you will likely want to use this in combination with a `RetryExecutor` like this: ```php $executor = new RetryExecutor( new TimeoutExecutor( new UdpTransportExecutor($loop), 3.0, $loop ) ); ``` > Internally, this class uses PHP's UDP sockets and does not take advantage of [react/datagram](https://github.com/reactphp/datagram) purely for organizational reasons to avoid a cyclic dependency between the two packages. Higher-level components should take advantage of the Datagram component instead of reimplementing this socket logic from scratch. ### HostsFileExecutor Note that the above `UdpTransportExecutor` class always performs an actual DNS query. If you also want to take entries from your hosts file into account, you may use this code: ```php $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); $executor = new UdpTransportExecutor($loop); $executor = new HostsFileExecutor($hosts, $executor); $executor->query( '8.8.8.8:53', new Query('localhost', Message::TYPE_A, Message::CLASS_IN) ); ``` ## Install The recommended way to install this library is [through Composer](https://getcomposer.org). [New to Composer?](https://getcomposer.org/doc/00-intro.md) This will install the latest supported version: ```bash $ composer require react/dns:^0.4.16 ``` See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. This project aims to run on any platform and thus does not require any PHP extensions and supports running on legacy PHP 5.3 through current PHP 7+ and HHVM. It's *highly recommended to use PHP 7+* for this project. ## Tests To run the test suite, you first need to clone this repo and then install all dependencies [through Composer](https://getcomposer.org): ```bash $ composer install ``` To run the test suite, go to the project root and run: ```bash $ php vendor/bin/phpunit ``` The test suite also contains a number of functional integration tests that rely on a stable internet connection. If you do not want to run these, they can simply be skipped like this: ```bash $ php vendor/bin/phpunit --exclude-group internet ``` ## License MIT, see [LICENSE file](LICENSE). ## References * [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities * [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification dns-0.4.16/composer.json000066400000000000000000000013421337201025100151020ustar00rootroot00000000000000{ "name": "react/dns", "description": "Async DNS resolver for ReactPHP", "keywords": ["dns", "dns-resolver", "ReactPHP", "async"], "license": "MIT", "require": { "php": ">=5.3.0", "react/cache": "^0.5 || ^0.4 || ^0.3", "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", "react/promise": "^2.1 || ^1.2.1", "react/promise-timer": "^1.2", "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5" }, "require-dev": { "clue/block-react": "^1.2", "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" }, "autoload": { "psr-4": { "React\\Dns\\": "src" } }, "autoload-dev": { "psr-4": { "React\\Tests\\Dns\\": "tests" } } } dns-0.4.16/examples/000077500000000000000000000000001337201025100141765ustar00rootroot00000000000000dns-0.4.16/examples/01-one.php000066400000000000000000000010461337201025100157070ustar00rootroot00000000000000nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new Factory(); $resolver = $factory->create($server, $loop); $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; $resolver->resolve($name)->then(function ($ip) use ($name) { echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; }, 'printf'); $loop->run(); dns-0.4.16/examples/02-concurrent.php000066400000000000000000000012131337201025100173050ustar00rootroot00000000000000nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new Factory(); $resolver = $factory->create($server, $loop); $names = array_slice($argv, 1); if (!$names) { $names = array('google.com', 'www.google.com', 'gmail.com'); } foreach ($names as $name) { $resolver->resolve($name)->then(function ($ip) use ($name) { echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; }, 'printf'); } $loop->run(); dns-0.4.16/examples/03-cached.php000066400000000000000000000022041337201025100163340ustar00rootroot00000000000000nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new Factory(); $resolver = $factory->createCached($server, $loop); $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; $resolver->resolve($name)->then(function ($ip) use ($name) { echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; }, 'printf'); $loop->addTimer(1.0, function() use ($name, $resolver) { $resolver->resolve($name)->then(function ($ip) use ($name) { echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; }, 'printf'); }); $loop->addTimer(2.0, function() use ($name, $resolver) { $resolver->resolve($name)->then(function ($ip) use ($name) { echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; }, 'printf'); }); $loop->addTimer(3.0, function() use ($name, $resolver) { $resolver->resolve($name)->then(function ($ip) use ($name) { echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; }, 'printf'); }); $loop->run(); dns-0.4.16/examples/11-all-ips.php000066400000000000000000000020171337201025100164670ustar00rootroot00000000000000nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new Factory(); $resolver = $factory->create($server, $loop); $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; $resolver->resolveAll($name, Message::TYPE_A)->then(function (array $ips) use ($name) { echo 'IPv4 addresses for ' . $name . ': ' . implode(', ', $ips) . PHP_EOL; }, function (Exception $e) use ($name) { echo 'No IPv4 addresses for ' . $name . ': ' . $e->getMessage() . PHP_EOL; }); $resolver->resolveAll($name, Message::TYPE_AAAA)->then(function (array $ips) use ($name) { echo 'IPv6 addresses for ' . $name . ': ' . implode(', ', $ips) . PHP_EOL; }, function (Exception $e) use ($name) { echo 'No IPv6 addresses for ' . $name . ': ' . $e->getMessage() . PHP_EOL; }); $loop->run(); dns-0.4.16/examples/12-all-types.php000066400000000000000000000012411337201025100170370ustar00rootroot00000000000000nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new Factory(); $resolver = $factory->create($server, $loop); $name = isset($argv[1]) ? $argv[1] : 'google.com'; $type = constant('React\Dns\Model\Message::TYPE_' . (isset($argv[2]) ? $argv[2] : 'TXT')); $resolver->resolveAll($name, $type)->then(function (array $values) { var_dump($values); }, function (Exception $e) { echo $e->getMessage() . PHP_EOL; }); $loop->run(); dns-0.4.16/examples/13-reverse-dns.php000066400000000000000000000016151337201025100173700ustar00rootroot00000000000000nameservers ? reset($config->nameservers) : '8.8.8.8'; $factory = new Factory(); $resolver = $factory->create($server, $loop); $ip = isset($argv[1]) ? $argv[1] : '8.8.8.8'; if (@inet_pton($ip) === false) { exit('Error: Given argument is not a valid IP' . PHP_EOL); } if (strpos($ip, ':') === false) { $name = inet_ntop(strrev(inet_pton($ip))) . '.in-addr.arpa'; } else { $name = wordwrap(strrev(bin2hex(inet_pton($ip))), 1, '.', true) . '.ip6.arpa'; } $resolver->resolveAll($name, Message::TYPE_PTR)->then(function (array $names) { var_dump($names); }, function (Exception $e) { echo $e->getMessage() . PHP_EOL; }); $loop->run(); dns-0.4.16/examples/91-query-a-and-aaaa.php000066400000000000000000000015211337201025100201410ustar00rootroot00000000000000query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) { foreach ($message->answers as $answer) { echo 'IPv4: ' . $answer->data . PHP_EOL; } }, 'printf'); $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) { foreach ($message->answers as $answer) { echo 'IPv6: ' . $answer->data . PHP_EOL; } }, 'printf'); $loop->run(); dns-0.4.16/examples/92-query-any.php000066400000000000000000000044221337201025100170730ustar00rootroot00000000000000query('8.8.8.8:53', $any)->then(function (Message $message) { foreach ($message->answers as $answer) { /* @var $answer Record */ $data = $answer->data; switch ($answer->type) { case Message::TYPE_A: $type = 'A'; break; case Message::TYPE_AAAA: $type = 'AAAA'; break; case Message::TYPE_NS: $type = 'NS'; break; case Message::TYPE_PTR: $type = 'PTR'; break; case Message::TYPE_CNAME: $type = 'CNAME'; break; case Message::TYPE_TXT: // TXT records can contain a list of (binary) strings for each record. // here, we assume this is printable ASCII and simply concatenate output $type = 'TXT'; $data = implode('', $data); break; case Message::TYPE_MX: // MX records contain "priority" and "target", only dump its values here $type = 'MX'; $data = implode(' ', $data); break; case Message::TYPE_SRV: // SRV records contains priority, weight, port and target, dump structure here $type = 'SRV'; $data = json_encode($data); break; case Message::TYPE_SOA: // SOA records contain structured data, dump structure here $type = 'SOA'; $data = json_encode($data); break; default: // unknown type uses HEX format $type = 'Type ' . $answer->type; $data = wordwrap(strtoupper(bin2hex($data)), 2, ' ', true); } echo $type . ': ' . $data . PHP_EOL; } }, 'printf'); $loop->run(); dns-0.4.16/phpunit.xml.dist000066400000000000000000000012321337201025100155310ustar00rootroot00000000000000 ./tests/ ./src/ dns-0.4.16/src/000077500000000000000000000000001337201025100131475ustar00rootroot00000000000000dns-0.4.16/src/BadServerException.php000066400000000000000000000001151337201025100174110ustar00rootroot00000000000000nameservers = $matches[1]; return $config; } /** * Loads the DNS configurations from Windows's WMIC (from the given command or default command) * * Note that this method blocks while loading the given command and should * thus be used with care! While this should be relatively fast for normal * WMIC commands, it remains unknown if this may block under certain * circumstances. In particular, this method should only be executed before * the loop starts, not while it is running. * * Note that this method will only try to execute the given command try to * parse its output, irrespective of whether this command exists. In * particular, this command is only available on Windows. Currently, this * will only parse valid nameserver entries from the command output and will * ignore all other output without complaining. * * Note that the previous section implies that this may return an empty * `Config` object if no valid nameserver entries can be found. * * @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing * @return self * @link https://ss64.com/nt/wmic.html */ public static function loadWmicBlocking($command = null) { $contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command); preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches); $config = new self(); $config->nameservers = $matches[1]; return $config; } public $nameservers = array(); } dns-0.4.16/src/Config/FilesystemFactory.php000066400000000000000000000036371337201025100205520ustar00rootroot00000000000000loop = $loop; } public function create($filename) { return $this ->loadEtcResolvConf($filename) ->then(array($this, 'parseEtcResolvConf')); } /** * @param string $contents * @return Promise * @deprecated see Config instead */ public function parseEtcResolvConf($contents) { return Promise\resolve(Config::loadResolvConfBlocking( 'data://text/plain;base64,' . base64_encode($contents) )); } public function loadEtcResolvConf($filename) { if (!file_exists($filename)) { return Promise\reject(new \InvalidArgumentException("The filename for /etc/resolv.conf given does not exist: $filename")); } try { $deferred = new Deferred(); $fd = fopen($filename, 'r'); stream_set_blocking($fd, 0); $contents = ''; $stream = class_exists('React\Stream\ReadableResourceStream') ? new ReadableResourceStream($fd, $this->loop) : new Stream($fd, $this->loop); $stream->on('data', function ($data) use (&$contents) { $contents .= $data; }); $stream->on('end', function () use (&$contents, $deferred) { $deferred->resolve($contents); }); $stream->on('error', function ($error) use ($deferred) { $deferred->reject($error); }); return $deferred->promise(); } catch (\Exception $e) { return Promise\reject($e); } } } dns-0.4.16/src/Config/HostsFile.php000066400000000000000000000111531337201025100167660ustar00rootroot00000000000000contents = $contents; } /** * Returns all IPs for the given hostname * * @param string $name * @return string[] */ public function getIpsForHost($name) { $name = strtolower($name); $ips = array(); foreach (preg_split('/\r?\n/', $this->contents) as $line) { $parts = preg_split('/\s+/', $line); $ip = array_shift($parts); if ($parts && array_search($name, $parts) !== false) { // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) { $ip = substr($ip, 0, $pos); } if (@inet_pton($ip) !== false) { $ips[] = $ip; } } } return $ips; } /** * Returns all hostnames for the given IPv4 or IPv6 address * * @param string $ip * @return string[] */ public function getHostsForIp($ip) { // check binary representation of IP to avoid string case and short notation $ip = @inet_pton($ip); if ($ip === false) { return array(); } $names = array(); foreach (preg_split('/\r?\n/', $this->contents) as $line) { $parts = preg_split('/\s+/', $line, null, PREG_SPLIT_NO_EMPTY); $addr = array_shift($parts); // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) { $addr = substr($addr, 0, $pos); } if (@inet_pton($addr) === $ip) { foreach ($parts as $part) { $names[] = $part; } } } return $names; } } dns-0.4.16/src/Model/000077500000000000000000000000001337201025100142075ustar00rootroot00000000000000dns-0.4.16/src/Model/HeaderBag.php000066400000000000000000000025141337201025100165240ustar00rootroot00000000000000 0, 'anCount' => 0, 'nsCount' => 0, 'arCount' => 0, 'qr' => 0, 'opcode' => Message::OPCODE_QUERY, 'aa' => 0, 'tc' => 0, 'rd' => 0, 'ra' => 0, 'z' => 0, 'rcode' => Message::RCODE_OK, ); /** * @deprecated unused, exists for BC only */ public $data = ''; public function get($name) { return isset($this->attributes[$name]) ? $this->attributes[$name] : null; } public function set($name, $value) { $this->attributes[$name] = $value; } public function isQuery() { return 0 === $this->attributes['qr']; } public function isResponse() { return 1 === $this->attributes['qr']; } public function isTruncated() { return 1 === $this->attributes['tc']; } public function populateCounts(Message $message) { $this->attributes['qdCount'] = count($message->questions); $this->attributes['anCount'] = count($message->answers); $this->attributes['nsCount'] = count($message->authority); $this->attributes['arCount'] = count($message->additional); } } dns-0.4.16/src/Model/Message.php000066400000000000000000000101261337201025100163040ustar00rootroot00000000000000header->set('id', self::generateId()); $request->header->set('rd', 1); $request->questions[] = (array) $query; $request->prepare(); return $request; } /** * Creates a new response message for the given query with the given answer records * * @param Query $query * @param Record[] $answers * @return self */ public static function createResponseWithAnswersForQuery(Query $query, array $answers) { $response = new Message(); $response->header->set('id', self::generateId()); $response->header->set('qr', 1); $response->header->set('opcode', Message::OPCODE_QUERY); $response->header->set('rd', 1); $response->header->set('rcode', Message::RCODE_OK); $response->questions[] = (array) $query; foreach ($answers as $record) { $response->answers[] = $record; } $response->prepare(); return $response; } /** * generates a random 16 bit message ID * * This uses a CSPRNG so that an outside attacker that is sending spoofed * DNS response messages can not guess the message ID to avoid possible * cache poisoning attacks. * * The `random_int()` function is only available on PHP 7+ or when * https://github.com/paragonie/random_compat is installed. As such, using * the latest supported PHP version is highly recommended. This currently * falls back to a less secure random number generator on older PHP versions * in the hope that this system is properly protected against outside * attackers, for example by using one of the common local DNS proxy stubs. * * @return int * @see self::getId() * @codeCoverageIgnore */ private static function generateId() { if (function_exists('random_int')) { return random_int(0, 0xffff); } return mt_rand(0, 0xffff); } public $header; public $questions = array(); public $answers = array(); public $authority = array(); public $additional = array(); /** * @deprecated still used internally for BC reasons, should not be used externally. */ public $data = ''; /** * @deprecated still used internally for BC reasons, should not be used externally. */ public $consumed = 0; public function __construct() { $this->header = new HeaderBag(); } /** * Returns the 16 bit message ID * * The response message ID has to match the request message ID. This allows * the receiver to verify this is the correct response message. An outside * attacker may try to inject fake responses by "guessing" the message ID, * so this should use a proper CSPRNG to avoid possible cache poisoning. * * @return int * @see self::generateId() */ public function getId() { return $this->header->get('id'); } /** * Returns the response code (RCODE) * * @return int see self::RCODE_* constants */ public function getResponseCode() { return $this->header->get('rcode'); } public function prepare() { $this->header->populateCounts($this); } } dns-0.4.16/src/Model/Record.php000066400000000000000000000104201337201025100161330ustar00rootroot00000000000000name = $name; $this->type = $type; $this->class = $class; $this->ttl = $ttl; $this->data = $data; } } dns-0.4.16/src/Protocol/000077500000000000000000000000001337201025100147505ustar00rootroot00000000000000dns-0.4.16/src/Protocol/BinaryDumper.php000066400000000000000000000031531337201025100200640ustar00rootroot00000000000000headerToBinary($message->header); $data .= $this->questionToBinary($message->questions); return $data; } private function headerToBinary(HeaderBag $header) { $data = ''; $data .= pack('n', $header->get('id')); $flags = 0x00; $flags = ($flags << 1) | $header->get('qr'); $flags = ($flags << 4) | $header->get('opcode'); $flags = ($flags << 1) | $header->get('aa'); $flags = ($flags << 1) | $header->get('tc'); $flags = ($flags << 1) | $header->get('rd'); $flags = ($flags << 1) | $header->get('ra'); $flags = ($flags << 3) | $header->get('z'); $flags = ($flags << 4) | $header->get('rcode'); $data .= pack('n', $flags); $data .= pack('n', $header->get('qdCount')); $data .= pack('n', $header->get('anCount')); $data .= pack('n', $header->get('nsCount')); $data .= pack('n', $header->get('arCount')); return $data; } private function questionToBinary(array $questions) { $data = ''; foreach ($questions as $question) { $labels = explode('.', $question['name']); foreach ($labels as $label) { $data .= chr(strlen($label)).$label; } $data .= "\x00"; $data .= pack('n*', $question['type'], $question['class']); } return $data; } } dns-0.4.16/src/Protocol/Parser.php000066400000000000000000000250121337201025100167150ustar00rootroot00000000000000parse($data, $message) !== $message) { throw new InvalidArgumentException('Unable to parse binary message'); } return $message; } /** * @deprecated unused, exists for BC only * @codeCoverageIgnore */ public function parseChunk($data, Message $message) { return $this->parse($data, $message); } private function parse($data, Message $message) { $message->data .= $data; if (!$message->header->get('id')) { if (!$this->parseHeader($message)) { return; } } if ($message->header->get('qdCount') != count($message->questions)) { if (!$this->parseQuestion($message)) { return; } } if ($message->header->get('anCount') != count($message->answers)) { if (!$this->parseAnswer($message)) { return; } } return $message; } public function parseHeader(Message $message) { if (!isset($message->data[12 - 1])) { return; } $header = substr($message->data, 0, 12); $message->consumed += 12; list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); $rcode = $fields & bindec('1111'); $z = ($fields >> 4) & bindec('111'); $ra = ($fields >> 7) & 1; $rd = ($fields >> 8) & 1; $tc = ($fields >> 9) & 1; $aa = ($fields >> 10) & 1; $opcode = ($fields >> 11) & bindec('1111'); $qr = ($fields >> 15) & 1; $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); foreach ($vars as $name => $value) { $message->header->set($name, $value); } return $message; } public function parseQuestion(Message $message) { $consumed = $message->consumed; list($labels, $consumed) = $this->readLabels($message->data, $consumed); if ($labels === null || !isset($message->data[$consumed + 4 - 1])) { return; } list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); $consumed += 4; $message->consumed = $consumed; $message->questions[] = array( 'name' => implode('.', $labels), 'type' => $type, 'class' => $class, ); if ($message->header->get('qdCount') != count($message->questions)) { return $this->parseQuestion($message); } return $message; } public function parseAnswer(Message $message) { $consumed = $message->consumed; list($name, $consumed) = $this->readDomain($message->data, $consumed); if ($name === null || !isset($message->data[$consumed + 10 - 1])) { return; } list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); $consumed += 4; list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4))); $consumed += 4; // TTL is a UINT32 that must not have most significant bit set for BC reasons if ($ttl < 0 || $ttl >= 1 << 31) { $ttl = 0; } list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2))); $consumed += 2; if (!isset($message->data[$consumed + $rdLength - 1])) { return; } $rdata = null; $expected = $consumed + $rdLength; if (Message::TYPE_A === $type) { if ($rdLength === 4) { $rdata = inet_ntop(substr($message->data, $consumed, $rdLength)); $consumed += $rdLength; } } elseif (Message::TYPE_AAAA === $type) { if ($rdLength === 16) { $rdata = inet_ntop(substr($message->data, $consumed, $rdLength)); $consumed += $rdLength; } } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) { list($rdata, $consumed) = $this->readDomain($message->data, $consumed); } elseif (Message::TYPE_TXT === $type) { $rdata = array(); while ($consumed < $expected) { $len = ord($message->data[$consumed]); $rdata[] = (string)substr($message->data, $consumed + 1, $len); $consumed += $len + 1; } } elseif (Message::TYPE_MX === $type) { if ($rdLength > 2) { list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2))); list($target, $consumed) = $this->readDomain($message->data, $consumed + 2); $rdata = array( 'priority' => $priority, 'target' => $target ); } } elseif (Message::TYPE_SRV === $type) { if ($rdLength > 6) { list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6))); list($target, $consumed) = $this->readDomain($message->data, $consumed + 6); $rdata = array( 'priority' => $priority, 'weight' => $weight, 'port' => $port, 'target' => $target ); } } elseif (Message::TYPE_SOA === $type) { list($mname, $consumed) = $this->readDomain($message->data, $consumed); list($rname, $consumed) = $this->readDomain($message->data, $consumed); if ($mname !== null && $rname !== null && isset($message->data[$consumed + 20 - 1])) { list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20))); $consumed += 20; $rdata = array( 'mname' => $mname, 'rname' => $rname, 'serial' => $serial, 'refresh' => $refresh, 'retry' => $retry, 'expire' => $expire, 'minimum' => $minimum ); } } else { // unknown types simply parse rdata as an opaque binary string $rdata = substr($message->data, $consumed, $rdLength); $consumed += $rdLength; } // ensure parsing record data consumes expact number of bytes indicated in record length if ($consumed !== $expected || $rdata === null) { return; } $message->consumed = $consumed; $record = new Record($name, $type, $class, $ttl, $rdata); $message->answers[] = $record; if ($message->header->get('anCount') != count($message->answers)) { return $this->parseAnswer($message); } return $message; } private function readDomain($data, $consumed) { list ($labels, $consumed) = $this->readLabels($data, $consumed); if ($labels === null) { return array(null, null); } return array(implode('.', $labels), $consumed); } private function readLabels($data, $consumed) { $labels = array(); while (true) { if (!isset($data[$consumed])) { return array(null, null); } $length = \ord($data[$consumed]); // end of labels reached if ($length === 0) { $consumed += 1; break; } // first two bits set? this is a compressed label (14 bit pointer offset) if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1])) { $offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]); if ($offset >= $consumed) { return array(null, null); } $consumed += 2; list($newLabels) = $this->readLabels($data, $offset); if ($newLabels === null) { return array(null, null); } $labels = array_merge($labels, $newLabels); break; } // length MUST be 0-63 (6 bits only) and data has to be large enough if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) { return array(null, null); } $labels[] = substr($data, $consumed + 1, $length); $consumed += $length + 1; } return array($labels, $consumed); } /** * @deprecated unused, exists for BC only * @codeCoverageIgnore */ public function isEndOfLabels($data, $consumed) { $length = ord(substr($data, $consumed, 1)); return 0 === $length; } /** * @deprecated unused, exists for BC only * @codeCoverageIgnore */ public function getCompressedLabel($data, $consumed) { list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); list($labels) = $this->readLabels($data, $nameOffset); return array($labels, $consumed); } /** * @deprecated unused, exists for BC only * @codeCoverageIgnore */ public function isCompressedLabel($data, $consumed) { $mask = 0xc000; // 1100000000000000 list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); return (bool) ($peek & $mask); } /** * @deprecated unused, exists for BC only * @codeCoverageIgnore */ public function getCompressedLabelOffset($data, $consumed) { $mask = 0x3fff; // 0011111111111111 list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); return array($peek & $mask, $consumed + 2); } /** * @deprecated unused, exists for BC only * @codeCoverageIgnore */ public function signedLongToUnsignedLong($i) { return $i & 0x80000000 ? $i - 0xffffffff : $i; } } dns-0.4.16/src/Query/000077500000000000000000000000001337201025100142545ustar00rootroot00000000000000dns-0.4.16/src/Query/CachedExecutor.php000066400000000000000000000027561337201025100176650ustar00rootroot00000000000000executor = $executor; $this->cache = $cache; } public function query($nameserver, Query $query) { $executor = $this->executor; $cache = $this->cache; return $this->cache ->lookup($query) ->then( function ($cachedRecords) use ($query) { return Message::createResponseWithAnswersForQuery($query, $cachedRecords); }, function () use ($executor, $cache, $nameserver, $query) { return $executor ->query($nameserver, $query) ->then(function ($response) use ($cache, $query) { $cache->storeResponseMessage($query->currentTime, $response); return $response; }); } ); } /** * @deprecated unused, exists for BC only */ public function buildResponse(Query $query, array $cachedRecords) { return Message::createResponseWithAnswersForQuery($query, $cachedRecords); } /** * @deprecated unused, exists for BC only */ protected function generateId() { return mt_rand(0, 0xffff); } } dns-0.4.16/src/Query/CancellationException.php000066400000000000000000000001351337201025100212370ustar00rootroot00000000000000loop = $loop; $this->parser = $parser; $this->dumper = $dumper; $this->timeout = $timeout; } public function query($nameserver, Query $query) { $request = Message::createRequestForQuery($query); $queryData = $this->dumper->toBinary($request); $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; return $this->doQuery($nameserver, $transport, $queryData, $query->name); } /** * @deprecated unused, exists for BC only */ public function prepareRequest(Query $query) { return Message::createRequestForQuery($query); } public function doQuery($nameserver, $transport, $queryData, $name) { // we only support UDP right now if ($transport !== 'udp') { return Promise\reject(new \RuntimeException( 'DNS query for ' . $name . ' failed: Requested transport "' . $transport . '" not available, only UDP is supported in this version' )); } $that = $this; $parser = $this->parser; $loop = $this->loop; // UDP connections are instant, so try this without a timer try { $conn = $this->createConnection($nameserver, $transport); } catch (\Exception $e) { return Promise\reject(new \RuntimeException('DNS query for ' . $name . ' failed: ' . $e->getMessage(), 0, $e)); } $deferred = new Deferred(function ($resolve, $reject) use (&$timer, $loop, &$conn, $name) { $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name))); if ($timer !== null) { $loop->cancelTimer($timer); } $conn->close(); }); $timer = null; if ($this->timeout !== null) { $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { $conn->close(); $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); }); } $conn->on('data', function ($data) use ($conn, $parser, $deferred, $timer, $loop, $name) { $conn->end(); if ($timer !== null) { $loop->cancelTimer($timer); } try { $response = $parser->parseMessage($data); } catch (\Exception $e) { $deferred->reject($e); return; } if ($response->header->isTruncated()) { $deferred->reject(new \RuntimeException('DNS query for ' . $name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported')); return; } $deferred->resolve($response); }); $conn->write($queryData); return $deferred->promise(); } /** * @deprecated unused, exists for BC only */ protected function generateId() { return mt_rand(0, 0xffff); } /** * @param string $nameserver * @param string $transport * @return \React\Stream\DuplexStreamInterface */ protected function createConnection($nameserver, $transport) { $fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); if ($fd === false) { throw new \RuntimeException('Unable to connect to DNS server: ' . $errstr, $errno); } // Instantiate stream instance around this stream resource. // This ought to be replaced with a datagram socket in the future. // Temporary work around for Windows 10: buffer whole UDP response // @coverageIgnoreStart if (!class_exists('React\Stream\Stream')) { // prefer DuplexResourceStream as of react/stream v0.7.0 $conn = new DuplexResourceStream($fd, $this->loop, -1); } else { // use legacy Stream class for react/stream < v0.7.0 $conn = new Stream($fd, $this->loop); $conn->bufferSize = null; } // @coverageIgnoreEnd return $conn; } } dns-0.4.16/src/Query/ExecutorInterface.php000066400000000000000000000001711337201025100204030ustar00rootroot00000000000000hosts = $hosts; $this->fallback = $fallback; } public function query($nameserver, Query $query) { if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) { // forward lookup for type A or AAAA $records = array(); $expectsColon = $query->type === Message::TYPE_AAAA; foreach ($this->hosts->getIpsForHost($query->name) as $ip) { // ensure this is an IPv4/IPV6 address according to query type if ((strpos($ip, ':') !== false) === $expectsColon) { $records[] = new Record($query->name, $query->type, $query->class, 0, $ip); } } if ($records) { return Promise\resolve( Message::createResponseWithAnswersForQuery($query, $records) ); } } elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) { // reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain $ip = $this->getIpFromHost($query->name); if ($ip !== null) { $records = array(); foreach ($this->hosts->getHostsForIp($ip) as $host) { $records[] = new Record($query->name, $query->type, $query->class, 0, $host); } if ($records) { return Promise\resolve( Message::createResponseWithAnswersForQuery($query, $records) ); } } } return $this->fallback->query($nameserver, $query); } private function getIpFromHost($host) { if (substr($host, -13) === '.in-addr.arpa') { // IPv4: read as IP and reverse bytes $ip = @inet_pton(substr($host, 0, -13)); if ($ip === false || isset($ip[4])) { return null; } return inet_ntop(strrev($ip)); } elseif (substr($host, -9) === '.ip6.arpa') { // IPv6: replace dots, reverse nibbles and interpret as hexadecimal string $ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9))))); if ($ip === false) { return null; } return $ip; } else { return null; } } } dns-0.4.16/src/Query/Query.php000066400000000000000000000016021337201025100160710ustar00rootroot00000000000000name = $name; $this->type = $type; $this->class = $class; $this->currentTime = $currentTime; } } dns-0.4.16/src/Query/RecordBag.php000066400000000000000000000007671337201025100166270ustar00rootroot00000000000000records[] = array($currentTime + $record->ttl, $record); } public function all() { return array_values(array_map( function ($value) { list($expiresAt, $record) = $value; return $record; }, $this->records )); } } dns-0.4.16/src/Query/RecordCache.php000066400000000000000000000074301337201025100171330ustar00rootroot00000000000000cache = $cache; } /** * Looks up the cache if there's a cached answer for the given query * * @param Query $query * @return PromiseInterface Promise resolves with array of Record objects on sucess * or rejects with mixed values when query is not cached already. */ public function lookup(Query $query) { $id = $this->serializeQueryToIdentity($query); $expiredAt = $this->expiredAt; return $this->cache ->get($id) ->then(function ($value) use ($query, $expiredAt) { // cache 0.5+ resolves with null on cache miss, return explicit cache miss here if ($value === null) { return Promise\reject(); } /* @var $recordBag RecordBag */ $recordBag = unserialize($value); // reject this cache hit if the query was started before the time we expired the cache? // todo: this is a legacy left over, this value is never actually set, so this never applies. // todo: this should probably validate the cache time instead. if (null !== $expiredAt && $expiredAt <= $query->currentTime) { return Promise\reject(); } return $recordBag->all(); }); } /** * Stores all records from this response message in the cache * * @param int $currentTime * @param Message $message * @uses self::storeRecord() */ public function storeResponseMessage($currentTime, Message $message) { foreach ($message->answers as $record) { $this->storeRecord($currentTime, $record); } } /** * Stores a single record from a response message in the cache * * @param int $currentTime * @param Record $record */ public function storeRecord($currentTime, Record $record) { $id = $this->serializeRecordToIdentity($record); $cache = $this->cache; $this->cache ->get($id) ->then( function ($value) { if ($value === null) { // cache 0.5+ cache miss resolves with null, return empty bag here return new RecordBag(); } // reuse existing bag on cache hit to append new record to it return unserialize($value); }, function ($e) { // legacy cache < 0.5 cache miss rejects promise, return empty bag here return new RecordBag(); } ) ->then(function (RecordBag $recordBag) use ($id, $currentTime, $record, $cache) { // add a record to the existing (possibly empty) record bag and save to cache $recordBag->set($currentTime, $record); $cache->set($id, serialize($recordBag)); }); } public function expire($currentTime) { $this->expiredAt = $currentTime; } public function serializeQueryToIdentity(Query $query) { return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); } public function serializeRecordToIdentity(Record $record) { return sprintf('%s:%s:%s', $record->name, $record->type, $record->class); } } dns-0.4.16/src/Query/RetryExecutor.php000066400000000000000000000047221337201025100176160ustar00rootroot00000000000000executor = $executor; $this->retries = $retries; } public function query($nameserver, Query $query) { return $this->tryQuery($nameserver, $query, $this->retries); } public function tryQuery($nameserver, Query $query, $retries) { $deferred = new Deferred(function () use (&$promise) { if ($promise instanceof CancellablePromiseInterface) { $promise->cancel(); } }); $success = function ($value) use ($deferred, &$errorback) { $errorback = null; $deferred->resolve($value); }; $executor = $this->executor; $errorback = function ($e) use ($deferred, &$promise, $nameserver, $query, $success, &$errorback, &$retries, $executor) { if (!$e instanceof TimeoutException) { $errorback = null; $deferred->reject($e); } elseif ($retries <= 0) { $errorback = null; $deferred->reject($e = new \RuntimeException( 'DNS query for ' . $query->name . ' failed: too many retries', 0, $e )); // avoid garbage references by replacing all closures in call stack. // what a lovely piece of code! $r = new \ReflectionProperty('Exception', 'trace'); $r->setAccessible(true); $trace = $r->getValue($e); foreach ($trace as &$one) { foreach ($one['args'] as &$arg) { if ($arg instanceof \Closure) { $arg = 'Object(' . \get_class($arg) . ')'; } } } $r->setValue($e, $trace); } else { --$retries; $promise = $executor->query($nameserver, $query)->then( $success, $errorback ); } }; $promise = $this->executor->query($nameserver, $query)->then( $success, $errorback ); return $deferred->promise(); } } dns-0.4.16/src/Query/TimeoutException.php000066400000000000000000000001211337201025100202640ustar00rootroot00000000000000executor = $executor; $this->loop = $loop; $this->timeout = $timeout; } public function query($nameserver, Query $query) { return Timer\timeout($this->executor->query($nameserver, $query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { if ($e instanceof Timer\TimeoutException) { $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e); } throw $e; }); } } dns-0.4.16/src/Query/UdpTransportExecutor.php000066400000000000000000000127661337201025100211650ustar00rootroot00000000000000query( * '8.8.8.8:53', * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) * )->then(function (Message $message) { * foreach ($message->answers as $answer) { * echo 'IPv6: ' . $answer->data . PHP_EOL; * } * }, 'printf'); * * $loop->run(); * ``` * * See also the [fourth example](examples). * * Note that this executor does not implement a timeout, so you will very likely * want to use this in combination with a `TimeoutExecutor` like this: * * ```php * $executor = new TimeoutExecutor( * new UdpTransportExecutor($loop), * 3.0, * $loop * ); * ``` * * Also note that this executor uses an unreliable UDP transport and that it * does not implement any retry logic, so you will likely want to use this in * combination with a `RetryExecutor` like this: * * ```php * $executor = new RetryExecutor( * new TimeoutExecutor( * new UdpTransportExecutor($loop), * 3.0, * $loop * ) * ); * ``` * * > Internally, this class uses PHP's UDP sockets and does not take advantage * of [react/datagram](https://github.com/reactphp/datagram) purely for * organizational reasons to avoid a cyclic dependency between the two * packages. Higher-level components should take advantage of the Datagram * component instead of reimplementing this socket logic from scratch. */ class UdpTransportExecutor implements ExecutorInterface { private $loop; private $parser; private $dumper; /** * @param LoopInterface $loop * @param null|Parser $parser optional/advanced: DNS protocol parser to use * @param null|BinaryDumper $dumper optional/advanced: DNS protocol dumper to use */ public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null) { if ($parser === null) { $parser = new Parser(); } if ($dumper === null) { $dumper = new BinaryDumper(); } $this->loop = $loop; $this->parser = $parser; $this->dumper = $dumper; } public function query($nameserver, Query $query) { $request = Message::createRequestForQuery($query); $queryData = $this->dumper->toBinary($request); if (isset($queryData[512])) { return \React\Promise\reject(new \RuntimeException( 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport' )); } // UDP connections are instant, so try connection without a loop or timeout $socket = @\stream_socket_client("udp://$nameserver", $errno, $errstr, 0); if ($socket === false) { return \React\Promise\reject(new \RuntimeException( 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', $errno )); } // set socket to non-blocking and immediately try to send (fill write buffer) \stream_set_blocking($socket, false); \fwrite($socket, $queryData); $loop = $this->loop; $deferred = new Deferred(function () use ($loop, $socket, $query) { // cancellation should remove socket from loop and close socket $loop->removeReadStream($socket); \fclose($socket); throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled'); }); $parser = $this->parser; $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request) { // try to read a single data packet from the DNS server // ignoring any errors, this is uses UDP packets and not a stream of data $data = @\fread($socket, 512); try { $response = $parser->parseMessage($data); } catch (\Exception $e) { // ignore and await next if we received an invalid message from remote server // this may as well be a fake response from an attacker (possible DOS) return; } // ignore and await next if we received an unexpected response ID // this may as well be a fake response from an attacker (possible cache poisoning) if ($response->getId() !== $request->getId()) { return; } // we only react to the first valid message, so remove socket from loop and close $loop->removeReadStream($socket); \fclose($socket); if ($response->header->isTruncated()) { $deferred->reject(new \RuntimeException('DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported')); return; } $deferred->resolve($response); }); return $deferred->promise(); } } dns-0.4.16/src/RecordNotFoundException.php000066400000000000000000000001221337201025100204250ustar00rootroot00000000000000addPortToServerIfMissing($nameserver); $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop)); return new Resolver($nameserver, $executor); } public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null) { if (!($cache instanceof CacheInterface)) { $cache = new ArrayCache(); } $nameserver = $this->addPortToServerIfMissing($nameserver); $executor = $this->decorateHostsFileExecutor($this->createCachedExecutor($loop, $cache)); return new Resolver($nameserver, $executor); } /** * Tries to load the hosts file and decorates the given executor on success * * @param ExecutorInterface $executor * @return ExecutorInterface * @codeCoverageIgnore */ private function decorateHostsFileExecutor(ExecutorInterface $executor) { try { $executor = new HostsFileExecutor( HostsFile::loadFromPathBlocking(), $executor ); } catch (\RuntimeException $e) { // ignore this file if it can not be loaded } // Windows does not store localhost in hosts file by default but handles this internally // To compensate for this, we explicitly use hard-coded defaults for localhost if (DIRECTORY_SEPARATOR === '\\') { $executor = new HostsFileExecutor( new HostsFile("127.0.0.1 localhost\n::1 localhost"), $executor ); } return $executor; } protected function createExecutor(LoopInterface $loop) { return new TimeoutExecutor( new UdpTransportExecutor($loop), 5.0, $loop ); } protected function createRetryExecutor(LoopInterface $loop) { return new RetryExecutor($this->createExecutor($loop)); } protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache) { return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache)); } protected function addPortToServerIfMissing($nameserver) { if (strpos($nameserver, '[') === false && substr_count($nameserver, ':') >= 2) { // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets $nameserver = '[' . $nameserver . ']'; } // assume a dummy scheme when checking for the port, otherwise parse_url() fails if (parse_url('dummy://' . $nameserver, PHP_URL_PORT) === null) { $nameserver .= ':53'; } return $nameserver; } } dns-0.4.16/src/Resolver/Resolver.php000066400000000000000000000214671337201025100172740ustar00rootroot00000000000000nameserver = $nameserver; $this->executor = $executor; } /** * Resolves the given $domain name to a single IPv4 address (type `A` query). * * ```php * $resolver->resolve('reactphp.org')->then(function ($ip) { * echo 'IP for reactphp.org is ' . $ip . PHP_EOL; * }); * ``` * * This is one of the main methods in this package. It sends a DNS query * for the given $domain name to your DNS server and returns a single IP * address on success. * * If the DNS server sends a DNS response message that contains more than * one IP address for this query, it will randomly pick one of the IP * addresses from the response. If you want the full list of IP addresses * or want to send a different type of query, you should use the * [`resolveAll()`](#resolveall) method instead. * * If the DNS server sends a DNS response message that indicates an error * code, this method will reject with a `RecordNotFoundException`. Its * message and code can be used to check for the response code. * * If the DNS communication fails and the server does not respond with a * valid response message, this message will reject with an `Exception`. * * Pending DNS queries can be cancelled by cancelling its pending promise like so: * * ```php * $promise = $resolver->resolve('reactphp.org'); * * $promise->cancel(); * ``` * * @param string $domain * @return PromiseInterface Returns a promise which resolves with a single IP address on success or * rejects with an Exception on error. */ public function resolve($domain) { return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) { return $ips[array_rand($ips)]; }); } /** * Resolves all record values for the given $domain name and query $type. * * ```php * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; * }); * * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; * }); * ``` * * This is one of the main methods in this package. It sends a DNS query * for the given $domain name to your DNS server and returns a list with all * record values on success. * * If the DNS server sends a DNS response message that contains one or more * records for this query, it will return a list with all record values * from the response. You can use the `Message::TYPE_*` constants to control * which type of query will be sent. Note that this method always returns a * list of record values, but each record value type depends on the query * type. For example, it returns the IPv4 addresses for type `A` queries, * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, * `CNAME` and `PTR` queries and structured data for other queries. See also * the `Record` documentation for more details. * * If the DNS server sends a DNS response message that indicates an error * code, this method will reject with a `RecordNotFoundException`. Its * message and code can be used to check for the response code. * * If the DNS communication fails and the server does not respond with a * valid response message, this message will reject with an `Exception`. * * Pending DNS queries can be cancelled by cancelling its pending promise like so: * * ```php * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); * * $promise->cancel(); * ``` * * @param string $domain * @return PromiseInterface Returns a promise which resolves with all record values on success or * rejects with an Exception on error. */ public function resolveAll($domain, $type) { $query = new Query($domain, $type, Message::CLASS_IN); $that = $this; return $this->executor->query( $this->nameserver, $query )->then(function (Message $response) use ($query, $that) { return $that->extractValues($query, $response); }); } /** * @deprecated unused, exists for BC only */ public function extractAddress(Query $query, Message $response) { $addresses = $this->extractValues($query, $response); return $addresses[array_rand($addresses)]; } /** * [Internal] extract all resource record values from response for this query * * @param Query $query * @param Message $response * @return array * @throws RecordNotFoundException when response indicates an error or contains no data * @internal */ public function extractValues(Query $query, Message $response) { // reject if response code indicates this is an error response message $code = $response->getResponseCode(); if ($code !== Message::RCODE_OK) { switch ($code) { case Message::RCODE_FORMAT_ERROR: $message = 'Format Error'; break; case Message::RCODE_SERVER_FAILURE: $message = 'Server Failure'; break; case Message::RCODE_NAME_ERROR: $message = 'Non-Existent Domain / NXDOMAIN'; break; case Message::RCODE_NOT_IMPLEMENTED: $message = 'Not Implemented'; break; case Message::RCODE_REFUSED: $message = 'Refused'; break; default: $message = 'Unknown error response code ' . $code; } throw new RecordNotFoundException( 'DNS query for ' . $query->name . ' returned an error response (' . $message . ')', $code ); } $answers = $response->answers; $addresses = $this->valuesByNameAndType($answers, $query->name, $query->type); // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found) if (0 === count($addresses)) { throw new RecordNotFoundException( 'DNS query for ' . $query->name . ' did not return a valid answer (NOERROR / NODATA)' ); } return array_values($addresses); } /** * @deprecated unused, exists for BC only */ public function resolveAliases(array $answers, $name) { return $this->valuesByNameAndType($answers, $name, Message::TYPE_A); } /** * @param \React\Dns\Model\Record[] $answers * @param string $name * @param int $type * @return array */ private function valuesByNameAndType(array $answers, $name, $type) { // return all record values for this name and type (if any) $named = $this->filterByName($answers, $name); $records = $this->filterByType($named, $type); if ($records) { return $this->mapRecordData($records); } // no matching records found? check if there are any matching CNAMEs instead $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME); if ($cnameRecords) { $cnames = $this->mapRecordData($cnameRecords); foreach ($cnames as $cname) { $records = array_merge( $records, $this->valuesByNameAndType($answers, $cname, $type) ); } } return $records; } private function filterByName(array $answers, $name) { return $this->filterByField($answers, 'name', $name); } private function filterByType(array $answers, $type) { return $this->filterByField($answers, 'type', $type); } private function filterByField(array $answers, $field, $value) { $value = strtolower($value); return array_filter($answers, function ($answer) use ($field, $value) { return $value === strtolower($answer->$field); }); } private function mapRecordData(array $records) { return array_map(function ($record) { return $record->data; }, $records); } } dns-0.4.16/tests/000077500000000000000000000000001337201025100135225ustar00rootroot00000000000000dns-0.4.16/tests/CallableStub.php000066400000000000000000000001451337201025100165700ustar00rootroot00000000000000assertInstanceOf('React\Dns\Config\Config', $config); } public function testLoadsDefaultPath() { if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Not supported on Windows'); } $config = Config::loadResolvConfBlocking(); $this->assertInstanceOf('React\Dns\Config\Config', $config); } public function testLoadsFromExplicitPath() { $config = Config::loadResolvConfBlocking(__DIR__ . '/../Fixtures/etc/resolv.conf'); $this->assertEquals(array('8.8.8.8'), $config->nameservers); } /** * @expectedException RuntimeException */ public function testLoadThrowsWhenPathIsInvalid() { Config::loadResolvConfBlocking(__DIR__ . '/invalid.conf'); } public function testParsesSingleEntryFile() { $contents = 'nameserver 8.8.8.8'; $expected = array('8.8.8.8'); $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); $this->assertEquals($expected, $config->nameservers); } public function testParsesNameserverEntriesFromAverageFileCorrectly() { $contents = '# # Mac OS X Notice # # This file is not used by the host name and address resolution # or the DNS query routing mechanisms used by most processes on # this Mac OS X system. # # This file is automatically generated. # domain v.cablecom.net nameserver 127.0.0.1 nameserver ::1 '; $expected = array('127.0.0.1', '::1'); $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); $this->assertEquals($expected, $config->nameservers); } public function testParsesEmptyFileWithoutNameserverEntries() { $contents = ''; $expected = array(); $config = Config::loadResolvConfBlocking('data://text/plain;base64,'); $this->assertEquals($expected, $config->nameservers); } public function testParsesFileAndIgnoresCommentsAndInvalidNameserverEntries() { $contents = ' # nameserver 1.2.3.4 ; nameserver 2.3.4.5 nameserver 3.4.5.6 # nope nameserver 4.5.6.7 5.6.7.8 nameserver 6.7.8.9 NameServer 7.8.9.10 '; $expected = array(); $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); $this->assertEquals($expected, $config->nameservers); } public function testLoadsFromWmicOnWindows() { if (DIRECTORY_SEPARATOR !== '\\') { $this->markTestSkipped('Only on Windows'); } $config = Config::loadWmicBlocking(); $this->assertInstanceOf('React\Dns\Config\Config', $config); } public function testLoadsSingleEntryFromWmicOutput() { $contents = ' Node,DNSServerSearchOrder ACE, ACE,{192.168.2.1} ACE, '; $expected = array('192.168.2.1'); $config = Config::loadWmicBlocking($this->echoCommand($contents)); $this->assertEquals($expected, $config->nameservers); } public function testLoadsEmptyListFromWmicOutput() { $contents = ' Node,DNSServerSearchOrder ACE, '; $expected = array(); $config = Config::loadWmicBlocking($this->echoCommand($contents)); $this->assertEquals($expected, $config->nameservers); } public function testLoadsSingleEntryForMultipleNicsFromWmicOutput() { $contents = ' Node,DNSServerSearchOrder ACE, ACE,{192.168.2.1} ACE, ACE,{192.168.2.2} ACE, '; $expected = array('192.168.2.1', '192.168.2.2'); $config = Config::loadWmicBlocking($this->echoCommand($contents)); $this->assertEquals($expected, $config->nameservers); } public function testLoadsMultipleEntriesForSingleNicWithSemicolonFromWmicOutput() { $contents = ' Node,DNSServerSearchOrder ACE, ACE,{192.168.2.1;192.168.2.2} ACE, '; $expected = array('192.168.2.1', '192.168.2.2'); $config = Config::loadWmicBlocking($this->echoCommand($contents)); $this->assertEquals($expected, $config->nameservers); } public function testLoadsMultipleEntriesForSingleNicWithQuotesFromWmicOutput() { $contents = ' Node,DNSServerSearchOrder ACE, ACE,{"192.168.2.1","192.168.2.2"} ACE, '; $expected = array('192.168.2.1', '192.168.2.2'); $config = Config::loadWmicBlocking($this->echoCommand($contents)); $this->assertEquals($expected, $config->nameservers); } private function echoCommand($output) { return 'echo ' . escapeshellarg($output); } } dns-0.4.16/tests/Config/FilesystemFactoryTest.php000066400000000000000000000040601337201025100217540ustar00rootroot00000000000000getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $factory = new FilesystemFactory($loop); $factory->parseEtcResolvConf($contents)->then(function ($config) use (&$capturedConfig) { $capturedConfig = $config; }); $this->assertNotNull($capturedConfig); $this->assertSame($expected, $capturedConfig->nameservers); } /** @test */ public function createShouldLoadStuffFromFilesystem() { $this->markTestIncomplete('Filesystem API is incomplete'); $expected = array('8.8.8.8'); $triggerListener = null; $capturedConfig = null; $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop ->expects($this->once()) ->method('addReadStream') ->will($this->returnCallback(function ($stream, $listener) use (&$triggerListener) { $triggerListener = function () use ($stream, $listener) { call_user_func($listener, $stream); }; })); $factory = new FilesystemFactory($loop); $factory->create(__DIR__.'/../Fixtures/etc/resolv.conf')->then(function ($config) use (&$capturedConfig) { $capturedConfig = $config; }); $triggerListener(); $this->assertNotNull($capturedConfig); $this->assertSame($expected, $capturedConfig->nameservers); } } dns-0.4.16/tests/Config/HostsFileTest.php000066400000000000000000000126771337201025100202150ustar00rootroot00000000000000assertInstanceOf('React\Dns\Config\HostsFile', $hosts); } public function testDefaultShouldHaveLocalhostMapped() { if (DIRECTORY_SEPARATOR === '\\') { $this->markTestSkipped('Not supported on Windows'); } $hosts = HostsFile::loadFromPathBlocking(); $this->assertContains('127.0.0.1', $hosts->getIpsForHost('localhost')); } /** * @expectedException RuntimeException */ public function testLoadThrowsForInvalidPath() { HostsFile::loadFromPathBlocking('does/not/exist'); } public function testContainsSingleLocalhostEntry() { $hosts = new HostsFile('127.0.0.1 localhost'); $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost')); $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); } public function testNonIpReturnsNothingForInvalidHosts() { $hosts = new HostsFile('a b'); $this->assertEquals(array(), $hosts->getIpsForHost('a')); $this->assertEquals(array(), $hosts->getIpsForHost('b')); } public function testIgnoresIpv6ZoneId() { $hosts = new HostsFile('fe80::1%lo0 localhost'); $this->assertEquals(array('fe80::1'), $hosts->getIpsForHost('localhost')); } public function testSkipsComments() { $hosts = new HostsFile('# start' . PHP_EOL .'#127.0.0.1 localhost' . PHP_EOL . '127.0.0.2 localhost # example.com'); $this->assertEquals(array('127.0.0.2'), $hosts->getIpsForHost('localhost')); $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); } public function testContainsSingleLocalhostEntryWithCaseIgnored() { $hosts = new HostsFile('127.0.0.1 LocalHost'); $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('LOCALHOST')); } public function testEmptyFileContainsNothing() { $hosts = new HostsFile(''); $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); } public function testSingleEntryWithMultipleNames() { $hosts = new HostsFile('127.0.0.1 localhost example.com'); $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('example.com')); $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost')); } public function testMergesEntriesOverMultipleLines() { $hosts = new HostsFile("127.0.0.1 localhost\n127.0.0.2 localhost\n127.0.0.3 a localhost b\n127.0.0.4 a localhost"); $this->assertEquals(array('127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'), $hosts->getIpsForHost('localhost')); } public function testMergesIpv4AndIpv6EntriesOverMultipleLines() { $hosts = new HostsFile("127.0.0.1 localhost\n::1 localhost"); $this->assertEquals(array('127.0.0.1', '::1'), $hosts->getIpsForHost('localhost')); } public function testReverseLookup() { $hosts = new HostsFile('127.0.0.1 localhost'); $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1')); $this->assertEquals(array(), $hosts->getHostsForIp('192.168.1.1')); } public function testReverseSkipsComments() { $hosts = new HostsFile("# start\n#127.0.0.1 localhosted\n127.0.0.2\tlocalhost\t# example.com\n\t127.0.0.3\t\texample.org\t\t"); $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1')); $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.2')); $this->assertEquals(array('example.org'), $hosts->getHostsForIp('127.0.0.3')); } public function testReverseNonIpReturnsNothing() { $hosts = new HostsFile('127.0.0.1 localhost'); $this->assertEquals(array(), $hosts->getHostsForIp('localhost')); $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1.1')); } public function testReverseNonIpReturnsNothingForInvalidHosts() { $hosts = new HostsFile('a b'); $this->assertEquals(array(), $hosts->getHostsForIp('a')); $this->assertEquals(array(), $hosts->getHostsForIp('b')); } public function testReverseLookupReturnsLowerCaseHost() { $hosts = new HostsFile('127.0.0.1 LocalHost'); $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1')); } public function testReverseLookupChecksNormalizedIpv6() { $hosts = new HostsFile('FE80::00a1 localhost'); $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::A1')); } public function testReverseLookupIgnoresIpv6ZoneId() { $hosts = new HostsFile('fe80::1%lo0 localhost'); $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::1')); } public function testReverseLookupReturnsMultipleHostsOverSingleLine() { $hosts = new HostsFile("::1 ip6-localhost ip6-loopback"); $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1')); } public function testReverseLookupReturnsMultipleHostsOverMultipleLines() { $hosts = new HostsFile("::1 ip6-localhost\n::1 ip6-loopback"); $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1')); } } dns-0.4.16/tests/Fixtures/000077500000000000000000000000001337201025100153335ustar00rootroot00000000000000dns-0.4.16/tests/Fixtures/etc/000077500000000000000000000000001337201025100161065ustar00rootroot00000000000000dns-0.4.16/tests/Fixtures/etc/resolv.conf000066400000000000000000000000231337201025100202620ustar00rootroot00000000000000nameserver 8.8.8.8 dns-0.4.16/tests/FunctionalResolverTest.php000066400000000000000000000057261337201025100207310ustar00rootroot00000000000000loop = LoopFactory::create(); $factory = new Factory(); $this->resolver = $factory->create('8.8.8.8', $this->loop); } public function testResolveLocalhostResolves() { $promise = $this->resolver->resolve('localhost'); $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); $this->loop->run(); } public function testResolveAllLocalhostResolvesWithArray() { $promise = $this->resolver->resolveAll('localhost', Message::TYPE_A); $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); $this->loop->run(); } /** * @group internet */ public function testResolveGoogleResolves() { $promise = $this->resolver->resolve('google.com'); $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); $this->loop->run(); } /** * @group internet */ public function testResolveAllGoogleMxResolvesWithCache() { $factory = new Factory(); $this->resolver = $factory->createCached('8.8.8.8', $this->loop); $promise = $this->resolver->resolveAll('google.com', Message::TYPE_MX); $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); $this->loop->run(); } /** * @group internet */ public function testResolveInvalidRejects() { $ex = $this->callback(function ($param) { return ($param instanceof RecordNotFoundException && $param->getCode() === Message::RCODE_NAME_ERROR); }); $promise = $this->resolver->resolve('example.invalid'); $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex)); $this->loop->run(); } public function testResolveCancelledRejectsImmediately() { $ex = $this->callback(function ($param) { return ($param instanceof \RuntimeException && $param->getMessage() === 'DNS query for google.com has been cancelled'); }); $promise = $this->resolver->resolve('google.com'); $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex)); $promise->cancel(); $time = microtime(true); $this->loop->run(); $time = microtime(true) - $time; $this->assertLessThan(0.1, $time); } public function testInvalidResolverDoesNotResolveGoogle() { $factory = new Factory(); $this->resolver = $factory->create('255.255.255.255', $this->loop); $promise = $this->resolver->resolve('google.com'); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); } } dns-0.4.16/tests/Model/000077500000000000000000000000001337201025100145625ustar00rootroot00000000000000dns-0.4.16/tests/Model/MessageTest.php000066400000000000000000000017751337201025100175310ustar00rootroot00000000000000assertTrue($request->header->isQuery()); $this->assertSame(1, $request->header->get('rd')); } public function testCreateResponseWithNoAnswers() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $answers = array(); $request = Message::createResponseWithAnswersForQuery($query, $answers); $this->assertFalse($request->header->isQuery()); $this->assertTrue($request->header->isResponse()); $this->assertEquals(0, $request->header->get('anCount')); $this->assertEquals(Message::RCODE_OK, $request->getResponseCode()); } } dns-0.4.16/tests/Protocol/000077500000000000000000000000001337201025100153235ustar00rootroot00000000000000dns-0.4.16/tests/Protocol/BinaryDumperTest.php000066400000000000000000000024361337201025100213020ustar00rootroot00000000000000formatHexDump(str_replace(' ', '', $data), 2); $request = new Message(); $request->header->set('id', 0x7262); $request->header->set('rd', 1); $request->questions[] = array( 'name' => 'igor.io', 'type' => Message::TYPE_A, 'class' => Message::CLASS_IN, ); $request->prepare(); $dumper = new BinaryDumper(); $data = $dumper->toBinary($request); $data = $this->convertBinaryToHexDump($data); $this->assertSame($expected, $data); } private function convertBinaryToHexDump($input) { return $this->formatHexDump(implode('', unpack('H*', $input))); } private function formatHexDump($input) { return implode(' ', str_split($input, 2)); } } dns-0.4.16/tests/Protocol/ParserTest.php000066400000000000000000001154661337201025100201450ustar00rootroot00000000000000parser = new Parser(); } /** * @dataProvider provideConvertTcpDumpToBinary */ public function testConvertTcpDumpToBinary($expected, $data) { $this->assertSame($expected, $this->convertTcpDumpToBinary($data)); } public function provideConvertTcpDumpToBinary() { return array( array(chr(0x72).chr(0x62), "72 62"), array(chr(0x72).chr(0x62).chr(0x01).chr(0x00), "72 62 01 00"), array(chr(0x72).chr(0x62).chr(0x01).chr(0x00).chr(0x00).chr(0x01), "72 62 01 00 00 01"), array(chr(0x01).chr(0x00).chr(0x01), "01 00 01"), ); } public function testParseRequest() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io $data .= "00 01 00 01"; // question: type A, class IN $data = $this->convertTcpDumpToBinary($data); $request = $this->parser->parseMessage($data); $header = $request->header; $this->assertSame(0x7262, $header->get('id')); $this->assertSame(1, $header->get('qdCount')); $this->assertSame(0, $header->get('anCount')); $this->assertSame(0, $header->get('nsCount')); $this->assertSame(0, $header->get('arCount')); $this->assertSame(0, $header->get('qr')); $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); $this->assertSame(0, $header->get('aa')); $this->assertSame(0, $header->get('tc')); $this->assertSame(1, $header->get('rd')); $this->assertSame(0, $header->get('ra')); $this->assertSame(0, $header->get('z')); $this->assertSame(Message::RCODE_OK, $header->get('rcode')); $this->assertCount(1, $request->questions); $this->assertSame('igor.io', $request->questions[0]['name']); $this->assertSame(Message::TYPE_A, $request->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']); } public function testParseResponse() { $data = ""; $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io $data .= "00 01 00 01"; // question: type A, class IN $data .= "c0 0c"; // answer: offset pointer to igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 04"; // answer: rdlength 4 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 $data = $this->convertTcpDumpToBinary($data); $response = $this->parser->parseMessage($data); $header = $response->header; $this->assertSame(0x7262, $header->get('id')); $this->assertSame(1, $header->get('qdCount')); $this->assertSame(1, $header->get('anCount')); $this->assertSame(0, $header->get('nsCount')); $this->assertSame(0, $header->get('arCount')); $this->assertSame(1, $header->get('qr')); $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); $this->assertSame(0, $header->get('aa')); $this->assertSame(0, $header->get('tc')); $this->assertSame(1, $header->get('rd')); $this->assertSame(1, $header->get('ra')); $this->assertSame(0, $header->get('z')); $this->assertSame(Message::RCODE_OK, $header->get('rcode')); $this->assertCount(1, $response->questions); $this->assertSame('igor.io', $response->questions[0]['name']); $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_A, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame('178.79.169.131', $response->answers[0]->data); } public function testParseQuestionWithTwoQuestions() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io $data .= "00 01 00 01"; // question: type A, class IN $data .= "03 77 77 77 04 69 67 6f 72 02 69 6f 00"; // question: www.igor.io $data .= "00 01 00 01"; // question: type A, class IN $data = $this->convertTcpDumpToBinary($data); $request = new Message(); $request->header->set('qdCount', 2); $request->data = $data; $this->parser->parseQuestion($request); $this->assertCount(2, $request->questions); $this->assertSame('igor.io', $request->questions[0]['name']); $this->assertSame(Message::TYPE_A, $request->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']); $this->assertSame('www.igor.io', $request->questions[1]['name']); $this->assertSame(Message::TYPE_A, $request->questions[1]['type']); $this->assertSame(Message::CLASS_IN, $request->questions[1]['class']); } public function testParseAnswerWithInlineData() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 04"; // answer: rdlength 4 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_A, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame('178.79.169.131', $response->answers[0]->data); } public function testParseAnswerWithExcessiveTtlReturnsZeroTtl() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "ff ff ff ff"; // answer: ttl 2^32 - 1 $data .= "00 04"; // answer: rdlength 4 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_A, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(0, $response->answers[0]->ttl); $this->assertSame('178.79.169.131', $response->answers[0]->data); } public function testParseAnswerWithTtlExactlyBoundaryReturnsZeroTtl() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "80 00 00 00"; // answer: ttl 2^31 $data .= "00 04"; // answer: rdlength 4 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_A, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(0, $response->answers[0]->ttl); $this->assertSame('178.79.169.131', $response->answers[0]->data); } public function testParseAnswerWithMaximumTtlReturnsExactTtl() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "7f ff ff ff"; // answer: ttl 2^31 - 1 $data .= "00 04"; // answer: rdlength 4 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_A, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(0x7fffffff, $response->answers[0]->ttl); $this->assertSame('178.79.169.131', $response->answers[0]->data); } public function testParseAnswerWithUnknownType() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "23 28 00 01"; // answer: type 9000, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 05"; // answer: rdlength 5 $data .= "68 65 6c 6c 6f"; // answer: rdata "hello" $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(9000, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame('hello', $response->answers[0]->data); } public function testParseResponseWithCnameAndOffsetPointers() { $data = ""; $data .= "9e 8d 81 80 00 01 00 01 00 00 00 00"; // header $data .= "04 6d 61 69 6c 06 67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: mail.google.com $data .= "00 05 00 01"; // question: type CNAME, class IN $data .= "c0 0c"; // answer: offset pointer to mail.google.com $data .= "00 05 00 01"; // answer: type CNAME, class IN $data .= "00 00 a8 9c"; // answer: ttl 43164 $data .= "00 0f"; // answer: rdlength 15 $data .= "0a 67 6f 6f 67 6c 65 6d 61 69 6c 01 6c"; // answer: rdata googlemail.l. $data .= "c0 11"; // answer: rdata offset pointer to google.com $data = $this->convertTcpDumpToBinary($data); $response = $this->parser->parseMessage($data); $this->assertCount(1, $response->questions); $this->assertSame('mail.google.com', $response->questions[0]['name']); $this->assertSame(Message::TYPE_CNAME, $response->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); $this->assertCount(1, $response->answers); $this->assertSame('mail.google.com', $response->answers[0]->name); $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(43164, $response->answers[0]->ttl); $this->assertSame('googlemail.l.google.com', $response->answers[0]->data); } public function testParseAAAAResponse() { $data = ""; $data .= "cd 72 81 80 00 01 00 01 00 00 00 00 06"; // header $data .= "67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: google.com $data .= "00 1c 00 01"; // question: type AAAA, class IN $data .= "c0 0c"; // answer: offset pointer to google.com $data .= "00 1c 00 01"; // answer: type AAAA, class IN $data .= "00 00 01 2b"; // answer: ttl 299 $data .= "00 10"; // answer: rdlength 16 $data .= "2a 00 14 50 40 09 08 09 00 00 00 00 00 00 20 0e"; // answer: 2a00:1450:4009:809::200e $data = $this->convertTcpDumpToBinary($data); $response = $this->parser->parseMessage($data); $header = $response->header; $this->assertSame(0xcd72, $header->get('id')); $this->assertSame(1, $header->get('qdCount')); $this->assertSame(1, $header->get('anCount')); $this->assertSame(0, $header->get('nsCount')); $this->assertSame(0, $header->get('arCount')); $this->assertSame(1, $header->get('qr')); $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); $this->assertSame(0, $header->get('aa')); $this->assertSame(0, $header->get('tc')); $this->assertSame(1, $header->get('rd')); $this->assertSame(1, $header->get('ra')); $this->assertSame(0, $header->get('z')); $this->assertSame(Message::RCODE_OK, $header->get('rcode')); $this->assertCount(1, $response->questions); $this->assertSame('google.com', $response->questions[0]['name']); $this->assertSame(Message::TYPE_AAAA, $response->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); $this->assertCount(1, $response->answers); $this->assertSame('google.com', $response->answers[0]->name); $this->assertSame(Message::TYPE_AAAA, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(299, $response->answers[0]->ttl); $this->assertSame('2a00:1450:4009:809::200e', $response->answers[0]->data); } public function testParseTXTResponse() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 10 00 01"; // answer: type TXT, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 06"; // answer: rdlength 6 $data .= "05 68 65 6c 6c 6f"; // answer: rdata length 5: hello $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_TXT, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame(array('hello'), $response->answers[0]->data); } public function testParseTXTResponseMultiple() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 10 00 01"; // answer: type TXT, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 0C"; // answer: rdlength 12 $data .= "05 68 65 6c 6c 6f 05 77 6f 72 6c 64"; // answer: rdata length 5: hello, length 5: world $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_TXT, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame(array('hello', 'world'), $response->answers[0]->data); } public function testParseMXResponse() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 0f 00 01"; // answer: type MX, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 09"; // answer: rdlength 9 $data .= "00 0a 05 68 65 6c 6c 6f 00"; // answer: rdata priority 10: hello $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_MX, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame(array('priority' => 10, 'target' => 'hello'), $response->answers[0]->data); } public function testParseSRVResponse() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 21 00 01"; // answer: type SRV, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 0C"; // answer: rdlength 12 $data .= "00 0a 00 14 1F 90 04 74 65 73 74 00"; // answer: rdata priority 10, weight 20, port 8080 test $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_SRV, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame( array( 'priority' => 10, 'weight' => 20, 'port' => 8080, 'target' => 'test' ), $response->answers[0]->data ); } public function testParseResponseWithTwoAnswers() { $data = ""; $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; // question: io.whois-servers.net $data .= "00 01 00 01"; // question: type A, class IN $data .= "c0 0c"; // answer: offset pointer to io.whois-servers.net $data .= "00 05 00 01"; // answer: type CNAME, class IN $data .= "00 00 00 29"; // answer: ttl 41 $data .= "00 0e"; // answer: rdlength 14 $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // answer: rdata whois.nic.io $data .= "c0 32"; // answer: offset pointer to whois.nic.io $data .= "00 01 00 01"; // answer: type CNAME, class IN $data .= "00 00 0d f7"; // answer: ttl 3575 $data .= "00 04"; // answer: rdlength 4 $data .= "c1 df 4e 98"; // answer: rdata 193.223.78.152 $data = $this->convertTcpDumpToBinary($data); $response = $this->parser->parseMessage($data); $this->assertCount(1, $response->questions); $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); $this->assertCount(2, $response->answers); $this->assertSame('io.whois-servers.net', $response->answers[0]->name); $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(41, $response->answers[0]->ttl); $this->assertSame('whois.nic.io', $response->answers[0]->data); $this->assertSame('whois.nic.io', $response->answers[1]->name); $this->assertSame(Message::TYPE_A, $response->answers[1]->type); $this->assertSame(Message::CLASS_IN, $response->answers[1]->class); $this->assertSame(3575, $response->answers[1]->ttl); $this->assertSame('193.223.78.152', $response->answers[1]->data); } public function testParseNSResponse() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 02 00 01"; // answer: type NS, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 07"; // answer: rdlength 7 $data .= "05 68 65 6c 6c 6f 00"; // answer: rdata hello $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_NS, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame('hello', $response->answers[0]->data); } public function testParseSOAResponse() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 06 00 01"; // answer: type SOA, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 27"; // answer: rdlength 39 $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600 $data .= "00 09 3a 80 00 00 0e 10"; // answer: 605800, 3600 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(1, $response->answers); $this->assertSame('igor.io', $response->answers[0]->name); $this->assertSame(Message::TYPE_SOA, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86400, $response->answers[0]->ttl); $this->assertSame( array( 'mname' => 'ns.hello', 'rname' => 'e.hello', 'serial' => 2018060501, 'refresh' => 10800, 'retry' => 3600, 'expire' => 604800, 'minimum' => 3600 ), $response->answers[0]->data ); } public function testParsePTRResponse() { $data = ""; $data .= "5d d8 81 80 00 01 00 01 00 00 00 00"; // header $data .= "01 34 01 34 01 38 01 38 07 69 6e"; // question: 4.4.8.8.in-addr.arpa $data .= "2d 61 64 64 72 04 61 72 70 61 00"; // question (continued) $data .= "00 0c 00 01"; // question: type PTR, class IN $data .= "c0 0c"; // answer: offset pointer to rdata $data .= "00 0c 00 01"; // answer: type PTR, class IN $data .= "00 01 51 7f"; // answer: ttl 86399 $data .= "00 20"; // answer: rdlength 32 $data .= "13 67 6f 6f 67 6c 65 2d 70 75 62 6c 69 63 2d 64"; // answer: rdata google-public-dns-b.google.com. $data .= "6e 73 2d 62 06 67 6f 6f 67 6c 65 03 63 6f 6d 00"; $data = $this->convertTcpDumpToBinary($data); $response = $this->parser->parseMessage($data); $header = $response->header; $this->assertSame(0x5dd8, $header->get('id')); $this->assertSame(1, $header->get('qdCount')); $this->assertSame(1, $header->get('anCount')); $this->assertSame(0, $header->get('nsCount')); $this->assertSame(0, $header->get('arCount')); $this->assertSame(1, $header->get('qr')); $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); $this->assertSame(0, $header->get('aa')); $this->assertSame(0, $header->get('tc')); $this->assertSame(1, $header->get('rd')); $this->assertSame(1, $header->get('ra')); $this->assertSame(0, $header->get('z')); $this->assertSame(Message::RCODE_OK, $header->get('rcode')); $this->assertCount(1, $response->questions); $this->assertSame('4.4.8.8.in-addr.arpa', $response->questions[0]['name']); $this->assertSame(Message::TYPE_PTR, $response->questions[0]['type']); $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); $this->assertCount(1, $response->answers); $this->assertSame('4.4.8.8.in-addr.arpa', $response->answers[0]->name); $this->assertSame(Message::TYPE_PTR, $response->answers[0]->type); $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); $this->assertSame(86399, $response->answers[0]->ttl); $this->assertSame('google-public-dns-b.google.com', $response->answers[0]->data); } /** * @expectedException InvalidArgumentException */ public function testParseIncompleteQuestionThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io //$data .= "00 01 00 01"; // question: type A, class IN $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseIncompleteQuestionLabelThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "04 69 67"; // question: ig …? $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseIncompleteQuestionNameThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "04 69 67 6f 72"; // question: igor. …? $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseIncompleteOffsetPointerInQuestionNameThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "ff"; // question: incomplete offset pointer $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseInvalidOffsetPointerInQuestionNameThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "ff ff"; // question: offset pointer to invalid address $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseInvalidOffsetPointerToSameLabelInQuestionNameThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "c0 0c"; // question: offset pointer to invalid address $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseInvalidOffsetPointerToStartOfMessageInQuestionNameThrows() { $data = ""; $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header $data .= "c0 00"; // question: offset pointer to start of message $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseIncompleteAnswerFieldsThrows() { $data = ""; $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io $data .= "00 01 00 01"; // question: type A, class IN $data .= "c0 0c"; // answer: offset pointer to igor.io $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } /** * @expectedException InvalidArgumentException */ public function testParseIncompleteAnswerRecordDataThrows() { $data = ""; $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io $data .= "00 01 00 01"; // question: type A, class IN $data .= "c0 0c"; // answer: offset pointer to igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 04"; // answer: rdlength 4 $data = $this->convertTcpDumpToBinary($data); $this->parser->parseMessage($data); } public function testParseInvalidNSResponseWhereDomainNameIsMissing() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 02 00 01"; // answer: type NS, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 00"; // answer: rdlength 0 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidAResponseWhereIPIsMissing() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 01 00 01"; // answer: type A, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 00"; // answer: rdlength 0 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidAAAAResponseWhereIPIsMissing() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 1c 00 01"; // answer: type AAAA, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 00"; // answer: rdlength 0 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidTXTResponseWhereTxtChunkExceedsLimit() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 10 00 01"; // answer: type TXT, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 06"; // answer: rdlength 6 $data .= "06 68 65 6c 6c 6f 6f"; // answer: rdata length 6: helloo $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidMXResponseWhereDomainNameIsIncomplete() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 0f 00 01"; // answer: type MX, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 08"; // answer: rdlength 8 $data .= "00 0a 05 68 65 6c 6c 6f"; // answer: rdata priority 10: hello (missing label end) $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidMXResponseWhereDomainNameIsMissing() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 0f 00 01"; // answer: type MX, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 02"; // answer: rdlength 2 $data .= "00 0a"; // answer: rdata priority 10 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidSRVResponseWhereDomainNameIsIncomplete() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 21 00 01"; // answer: type SRV, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 0b"; // answer: rdlength 11 $data .= "00 0a 00 14 1F 90 04 74 65 73 74"; // answer: rdata priority 10, weight 20, port 8080 test (missing label end) $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidSRVResponseWhereDomainNameIsMissing() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 21 00 01"; // answer: type SRV, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 06"; // answer: rdlength 6 $data .= "00 0a 00 14 1F 90"; // answer: rdata priority 10, weight 20, port 8080 $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } public function testParseInvalidSOAResponseWhereFlagsAreMissing() { $data = ""; $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io $data .= "00 06 00 01"; // answer: type SOA, class IN $data .= "00 01 51 80"; // answer: ttl 86400 $data .= "00 13"; // answer: rdlength 19 $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) $data = $this->convertTcpDumpToBinary($data); $response = new Message(); $response->header->set('anCount', 1); $response->data = $data; $this->parser->parseAnswer($response); $this->assertCount(0, $response->answers); } private function convertTcpDumpToBinary($input) { // sudo ngrep -d en1 -x port 53 return pack('H*', str_replace(' ', '', $input)); } } dns-0.4.16/tests/Query/000077500000000000000000000000001337201025100146275ustar00rootroot00000000000000dns-0.4.16/tests/Query/CachedExecutorTest.php000066400000000000000000000066151337201025100210760ustar00rootroot00000000000000createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnValue($this->createPromiseMock())); $cache = $this->getMockBuilder('React\Dns\Query\RecordCache') ->disableOriginalConstructor() ->getMock(); $cache ->expects($this->once()) ->method('lookup') ->will($this->returnValue(Promise\reject())); $cachedExecutor = new CachedExecutor($executor, $cache); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $cachedExecutor->query('8.8.8.8', $query); } /** * @covers React\Dns\Query\CachedExecutor * @test */ public function callingQueryTwiceShouldUseCachedResult() { $cachedRecords = array(new Record('igor.io', Message::TYPE_A, Message::CLASS_IN)); $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->will($this->callQueryCallbackWithAddress('178.79.169.131')); $cache = $this->getMockBuilder('React\Dns\Query\RecordCache') ->disableOriginalConstructor() ->getMock(); $cache ->expects($this->at(0)) ->method('lookup') ->with($this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnValue(Promise\reject())); $cache ->expects($this->at(1)) ->method('storeResponseMessage') ->with($this->isType('integer'), $this->isInstanceOf('React\Dns\Model\Message')); $cache ->expects($this->at(2)) ->method('lookup') ->with($this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnValue(Promise\resolve($cachedRecords))); $cachedExecutor = new CachedExecutor($executor, $cache); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {}); $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {}); } private function callQueryCallbackWithAddress($address) { return $this->returnCallback(function ($nameserver, $query) use ($address) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, $address); return Promise\resolve($response); }); } private function createExecutorMock() { return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); } private function createPromiseMock() { return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); } } dns-0.4.16/tests/Query/ExecutorTest.php000066400000000000000000000233511337201025100200020ustar00rootroot00000000000000loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $this->parser = $this->getMockBuilder('React\Dns\Protocol\Parser')->getMock(); $this->dumper = new BinaryDumper(); $this->executor = new Executor($this->loop, $this->parser, $this->dumper); } /** @test */ public function queryShouldCreateUdpRequest() { $timer = $this->createTimerMock(); $this->loop ->expects($this->any()) ->method('addTimer') ->will($this->returnValue($timer)); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->once()) ->method('createConnection') ->with('8.8.8.8:53', 'udp') ->will($this->returnNewConnectionMock(false)); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $this->executor->query('8.8.8.8:53', $query); } /** @test */ public function resolveShouldRejectIfRequestIsLargerThan512Bytes() { $query = new Query(str_repeat('a', 512).'.igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $this->setExpectedException('RuntimeException', 'DNS query for ' . $query->name . ' failed: Requested transport "tcp" not available, only UDP is supported in this version'); Block\await($promise, $this->loop); } /** @test */ public function resolveShouldCloseConnectionWhenCancelled() { $conn = $this->createConnectionMock(false); $conn->expects($this->once())->method('close'); $timer = $this->createTimerMock(); $this->loop ->expects($this->any()) ->method('addTimer') ->will($this->returnValue($timer)); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->once()) ->method('createConnection') ->with('8.8.8.8:53', 'udp') ->will($this->returnValue($conn)); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $promise->cancel(); $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled'); Block\await($promise, $this->loop); } /** @test */ public function resolveShouldNotStartOrCancelTimerWhenCancelledWithTimeoutIsNull() { $this->loop ->expects($this->never()) ->method('addTimer'); $this->executor = new Executor($this->loop, $this->parser, $this->dumper, null); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('127.0.0.1:53', $query); $promise->cancel(); $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled'); Block\await($promise, $this->loop); } /** @test */ public function resolveShouldRejectIfResponseIsTruncated() { $timer = $this->createTimerMock(); $this->loop ->expects($this->any()) ->method('addTimer') ->will($this->returnValue($timer)); $this->parser ->expects($this->once()) ->method('parseMessage') ->will($this->returnTruncatedResponse()); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->once()) ->method('createConnection') ->with('8.8.8.8:53', 'udp') ->will($this->returnNewConnectionMock()); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $this->executor->query('8.8.8.8:53', $query); } /** @test */ public function resolveShouldFailIfUdpThrow() { $this->loop ->expects($this->never()) ->method('addTimer'); $this->parser ->expects($this->never()) ->method('parseMessage'); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->once()) ->method('createConnection') ->with('8.8.8.8:53', 'udp') ->will($this->throwException(new \Exception('Nope'))); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $this->setExpectedException('RuntimeException', 'DNS query for igor.io failed: Nope'); Block\await($promise, $this->loop); } /** @test */ public function resolveShouldCancelTimerWhenFullResponseIsReceived() { $conn = $this->createConnectionMock(); $this->parser ->expects($this->once()) ->method('parseMessage') ->will($this->returnStandardResponse()); $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->at(0)) ->method('createConnection') ->with('8.8.8.8:53', 'udp') ->will($this->returnNewConnectionMock()); $timer = $this->createTimerMock(); $this->loop ->expects($this->once()) ->method('addTimer') ->with(5, $this->isInstanceOf('Closure')) ->will($this->returnValue($timer)); $this->loop ->expects($this->once()) ->method('cancelTimer') ->with($timer); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $this->executor->query('8.8.8.8:53', $query); } /** @test */ public function resolveShouldCloseConnectionOnTimeout() { $this->executor = $this->createExecutorMock(); $this->executor ->expects($this->at(0)) ->method('createConnection') ->with('8.8.8.8:53', 'udp') ->will($this->returnNewConnectionMock(false)); $timer = $this->createTimerMock(); $this->loop ->expects($this->never()) ->method('cancelTimer'); $this->loop ->expects($this->once()) ->method('addTimer') ->with(5, $this->isInstanceOf('Closure')) ->will($this->returnCallback(function ($time, $callback) use (&$timerCallback, $timer) { $timerCallback = $callback; return $timer; })); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $this->assertNotNull($timerCallback); $timerCallback(); $this->setExpectedException('React\Dns\Query\TimeoutException', 'DNS query for igor.io timed out'); Block\await($promise, $this->loop); } private function returnStandardResponse() { $that = $this; $callback = function ($data) use ($that) { $response = new Message(); $that->convertMessageToStandardResponse($response); return $response; }; return $this->returnCallback($callback); } private function returnTruncatedResponse() { $that = $this; $callback = function ($data) use ($that) { $response = new Message(); $that->convertMessageToTruncatedResponse($response); return $response; }; return $this->returnCallback($callback); } public function convertMessageToStandardResponse(Message $response) { $response->header->set('qr', 1); $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); $response->prepare(); return $response; } public function convertMessageToTruncatedResponse(Message $response) { $this->convertMessageToStandardResponse($response); $response->header->set('tc', 1); $response->prepare(); return $response; } private function returnNewConnectionMock($emitData = true) { $conn = $this->createConnectionMock($emitData); $callback = function () use ($conn) { return $conn; }; return $this->returnCallback($callback); } private function createConnectionMock($emitData = true) { $conn = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock(); $conn ->expects($this->any()) ->method('on') ->with('data', $this->isInstanceOf('Closure')) ->will($this->returnCallback(function ($name, $callback) use ($emitData) { $emitData && $callback(null); })); return $conn; } private function createTimerMock() { return $this->getMockBuilder( interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface' )->getMock(); } private function createExecutorMock() { return $this->getMockBuilder('React\Dns\Query\Executor') ->setConstructorArgs(array($this->loop, $this->parser, $this->dumper)) ->setMethods(array('createConnection')) ->getMock(); } } dns-0.4.16/tests/Query/HostsFileExecutorTest.php000066400000000000000000000121011337201025100216120ustar00rootroot00000000000000hosts = $this->getMockBuilder('React\Dns\Config\HostsFile')->disableOriginalConstructor()->getMock(); $this->fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); $this->executor = new HostsFileExecutor($this->hosts, $this->fallback); } public function testDoesNotTryToGetIpsForMxQuery() { $this->hosts->expects($this->never())->method('getIpsForHost'); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_MX, Message::CLASS_IN, 0)); } public function testFallsBackIfNoIpsWereFound() { $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array()); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); } public function testReturnsResponseMessageIfIpsWereFound() { $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1')); $this->fallback->expects($this->never())->method('query'); $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); } public function testFallsBackIfNoIpv4Matches() { $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1')); $this->fallback->expects($this->once())->method('query'); $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); } public function testReturnsResponseMessageIfIpv6AddressesWereFound() { $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1')); $this->fallback->expects($this->never())->method('query'); $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0)); } public function testFallsBackIfNoIpv6Matches() { $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1')); $this->fallback->expects($this->once())->method('query'); $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0)); } public function testDoesReturnReverseIpv4Lookup() { $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array('localhost')); $this->fallback->expects($this->never())->method('query'); $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); } public function testFallsBackIfNoReverseIpv4Matches() { $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array()); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); } public function testDoesReturnReverseIpv6Lookup() { $this->hosts->expects($this->once())->method('getHostsForIp')->with('2a02:2e0:3fe:100::6')->willReturn(array('ip6-localhost')); $this->fallback->expects($this->never())->method('query'); $this->executor->query('8.8.8.8', new Query('6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.e.f.3.0.0.e.2.0.2.0.a.2.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); } public function testFallsBackForInvalidAddress() { $this->hosts->expects($this->never())->method('getHostsForIp'); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('example.com', Message::TYPE_PTR, Message::CLASS_IN, 0)); } public function testReverseFallsBackForInvalidIpv4Address() { $this->hosts->expects($this->never())->method('getHostsForIp'); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('::1.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); } public function testReverseFallsBackForInvalidLengthIpv6Address() { $this->hosts->expects($this->never())->method('getHostsForIp'); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('abcd.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); } public function testReverseFallsBackForInvalidHexIpv6Address() { $this->hosts->expects($this->never())->method('getHostsForIp'); $this->fallback->expects($this->once())->method('query'); $this->executor->query('8.8.8.8', new Query('zZz.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); } } dns-0.4.16/tests/Query/RecordBagTest.php000066400000000000000000000052471337201025100200400ustar00rootroot00000000000000assertSame(array(), $recordBag->all()); } /** * @covers React\Dns\Query\RecordBag * @test */ public function setShouldSetTheValue() { $currentTime = 1345656451; $recordBag = new RecordBag(); $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600)); $records = $recordBag->all(); $this->assertCount(1, $records); $this->assertSame('igor.io', $records[0]->name); $this->assertSame(Message::TYPE_A, $records[0]->type); $this->assertSame(Message::CLASS_IN, $records[0]->class); } /** * @covers React\Dns\Query\RecordBag * @test */ public function setShouldAcceptMxRecord() { $currentTime = 1345656451; $recordBag = new RecordBag(); $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_MX, Message::CLASS_IN, 3600, array('priority' => 10, 'target' => 'igor.io'))); $records = $recordBag->all(); $this->assertCount(1, $records); $this->assertSame('igor.io', $records[0]->name); $this->assertSame(Message::TYPE_MX, $records[0]->type); $this->assertSame(Message::CLASS_IN, $records[0]->class); $this->assertSame(array('priority' => 10, 'target' => 'igor.io'), $records[0]->data); } /** * @covers React\Dns\Query\RecordBag * @test */ public function setShouldSetManyValues() { $currentTime = 1345656451; $recordBag = new RecordBag(); $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132')); $records = $recordBag->all(); $this->assertCount(2, $records); $this->assertSame('igor.io', $records[0]->name); $this->assertSame(Message::TYPE_A, $records[0]->type); $this->assertSame(Message::CLASS_IN, $records[0]->class); $this->assertSame('178.79.169.131', $records[0]->data); $this->assertSame('igor.io', $records[1]->name); $this->assertSame(Message::TYPE_A, $records[1]->type); $this->assertSame(Message::CLASS_IN, $records[1]->class); $this->assertSame('178.79.169.132', $records[1]->data); } } dns-0.4.16/tests/Query/RecordCacheTest.php000066400000000000000000000156031337201025100203470ustar00rootroot00000000000000getMockBuilder('React\Cache\CacheInterface')->getMock(); $base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); $cache = new RecordCache($base); $promise = $cache->lookup($query); $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); } /** * @covers React\Dns\Query\RecordCache * @test */ public function lookupOnLegacyCacheMissShouldReturnNull() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $base->expects($this->once())->method('get')->willReturn(\React\Promise\reject()); $cache = new RecordCache($base); $promise = $cache->lookup($query); $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); } /** * @covers React\Dns\Query\RecordCache * @test */ public function storeRecordPendingCacheDoesNotSetCache() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $pending = new Promise(function () { }); $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $base->expects($this->once())->method('get')->willReturn($pending); $base->expects($this->never())->method('set'); $cache = new RecordCache($base); $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); } /** * @covers React\Dns\Query\RecordCache * @test */ public function storeRecordOnNewCacheMissSetsCache() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); $base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string')); $cache = new RecordCache($base); $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); } /** * @covers React\Dns\Query\RecordCache * @test */ public function storeRecordOnOldCacheMissSetsCache() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $base->expects($this->once())->method('get')->willReturn(\React\Promise\reject()); $base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string')); $cache = new RecordCache($base); $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); } /** * @covers React\Dns\Query\RecordCache * @test */ public function storeRecordShouldMakeLookupSucceed() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $cache = new RecordCache(new ArrayCache()); $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); $promise = $cache->lookup($query); $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); $cachedRecords = $this->getPromiseValue($promise); $this->assertCount(1, $cachedRecords); $this->assertSame('178.79.169.131', $cachedRecords[0]->data); } /** * @covers React\Dns\Query\RecordCache * @test */ public function storeTwoRecordsShouldReturnBoth() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $cache = new RecordCache(new ArrayCache()); $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132')); $promise = $cache->lookup($query); $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); $cachedRecords = $this->getPromiseValue($promise); $this->assertCount(2, $cachedRecords); $this->assertSame('178.79.169.131', $cachedRecords[0]->data); $this->assertSame('178.79.169.132', $cachedRecords[1]->data); } /** * @covers React\Dns\Query\RecordCache * @test */ public function storeResponseMessageShouldStoreAllAnswerValues() { $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $response = new Message(); $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'); $response->prepare(); $cache = new RecordCache(new ArrayCache()); $cache->storeResponseMessage($query->currentTime, $response); $promise = $cache->lookup($query); $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); $cachedRecords = $this->getPromiseValue($promise); $this->assertCount(2, $cachedRecords); $this->assertSame('178.79.169.131', $cachedRecords[0]->data); $this->assertSame('178.79.169.132', $cachedRecords[1]->data); } /** * @covers React\Dns\Query\RecordCache * @test */ public function expireShouldExpireDeadRecords() { $cachedTime = 1345656451; $currentTime = $cachedTime + 3605; $cache = new RecordCache(new ArrayCache()); $cache->storeRecord($cachedTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); $cache->expire($currentTime); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, $currentTime); $promise = $cache->lookup($query); $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); } private function getPromiseValue(PromiseInterface $promise) { $capturedValue = null; $promise->then(function ($value) use (&$capturedValue) { $capturedValue = $value; }); return $capturedValue; } } dns-0.4.16/tests/Query/RetryExecutorTest.php000066400000000000000000000262671337201025100210410ustar00rootroot00000000000000createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnValue($this->expectPromiseOnce())); $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldRetryQueryOnTimeout() { $response = $this->createStandardResponse(); $executor = $this->createExecutorMock(); $executor ->expects($this->exactly(2)) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->onConsecutiveCalls( $this->returnCallback(function ($domain, $query) { return Promise\reject(new TimeoutException("timeout")); }), $this->returnCallback(function ($domain, $query) use ($response) { return Promise\resolve($response); }) )); $callback = $this->createCallableMock(); $callback ->expects($this->once()) ->method('__invoke') ->with($this->isInstanceOf('React\Dns\Model\Message')); $errorback = $this->expectCallableNever(); $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldStopRetryingAfterSomeAttempts() { $executor = $this->createExecutorMock(); $executor ->expects($this->exactly(3)) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($domain, $query) { return Promise\reject(new TimeoutException("timeout")); })); $callback = $this->expectCallableNever(); $errorback = $this->createCallableMock(); $errorback ->expects($this->once()) ->method('__invoke') ->with($this->isInstanceOf('RuntimeException')); $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldForwardNonTimeoutErrors() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($domain, $query) { return Promise\reject(new \Exception); })); $callback = $this->expectCallableNever(); $errorback = $this->createCallableMock(); $errorback ->expects($this->once()) ->method('__invoke') ->with($this->isInstanceOf('Exception')); $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldCancelQueryOnCancel() { $cancelled = 0; $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { ++$cancelled; $reject(new CancellationException('Cancelled')); }); return $deferred->promise(); }) ); $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $retryExecutor->query('8.8.8.8', $query); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); $this->assertEquals(0, $cancelled); $promise->cancel(); $this->assertEquals(1, $cancelled); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldCancelSecondQueryOnCancel() { $deferred = new Deferred(); $cancelled = 0; $executor = $this->createExecutorMock(); $executor ->expects($this->exactly(2)) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->onConsecutiveCalls( $this->returnValue($deferred->promise()), $this->returnCallback(function ($domain, $query) use (&$cancelled) { $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { ++$cancelled; $reject(new CancellationException('Cancelled')); }); return $deferred->promise(); }) )); $retryExecutor = new RetryExecutor($executor, 2); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $retryExecutor->query('8.8.8.8', $query); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); // first query will time out after a while and this sends the next query $deferred->reject(new TimeoutException()); $this->assertEquals(0, $cancelled); $promise->cancel(); $this->assertEquals(1, $cancelled); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldNotCauseGarbageReferencesOnSuccess() { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); } $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->willReturn(Promise\resolve($this->createStandardResponse())); $retryExecutor = new RetryExecutor($executor, 0); gc_collect_cycles(); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query); $this->assertEquals(0, gc_collect_cycles()); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldNotCauseGarbageReferencesOnTimeoutErrors() { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); } $executor = $this->createExecutorMock(); $executor ->expects($this->any()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->willReturn(Promise\reject(new TimeoutException("timeout"))); $retryExecutor = new RetryExecutor($executor, 0); gc_collect_cycles(); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query); $this->assertEquals(0, gc_collect_cycles()); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldNotCauseGarbageReferencesOnCancellation() { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); } $deferred = new Deferred(function () { throw new \RuntimeException(); }); $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->willReturn($deferred->promise()); $retryExecutor = new RetryExecutor($executor, 0); gc_collect_cycles(); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $retryExecutor->query('8.8.8.8', $query); $promise->cancel(); $promise = null; $this->assertEquals(0, gc_collect_cycles()); } /** * @covers React\Dns\Query\RetryExecutor * @test */ public function queryShouldNotCauseGarbageReferencesOnNonTimeoutErrors() { if (class_exists('React\Promise\When')) { $this->markTestSkipped('Not supported on legacy Promise v1 API'); } $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($domain, $query) { return Promise\reject(new \Exception); })); $retryExecutor = new RetryExecutor($executor, 2); gc_collect_cycles(); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $retryExecutor->query('8.8.8.8', $query); $this->assertEquals(0, gc_collect_cycles()); } protected function expectPromiseOnce($return = null) { $mock = $this->createPromiseMock(); $mock ->expects($this->once()) ->method('then') ->will($this->returnValue($return)); return $mock; } protected function createExecutorMock() { return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); } protected function createPromiseMock() { return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); } protected function createStandardResponse() { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); $response->prepare(); return $response; } } dns-0.4.16/tests/Query/TimeoutExecutorTest.php000066400000000000000000000073031337201025100213500ustar00rootroot00000000000000loop = Factory::create(); $this->wrapped = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); $this->executor = new TimeoutExecutor($this->wrapped, 5.0, $this->loop); } public function testCancellingPromiseWillCancelWrapped() { $cancelled = 0; $this->wrapped ->expects($this->once()) ->method('query') ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { ++$cancelled; $reject(new CancellationException('Cancelled')); }); return $deferred->promise(); })); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $this->assertEquals(0, $cancelled); $promise->cancel(); $this->assertEquals(1, $cancelled); $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); } public function testResolvesPromiseWhenWrappedResolves() { $this->wrapped ->expects($this->once()) ->method('query') ->willReturn(Promise\resolve('0.0.0.0')); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); } public function testRejectsPromiseWhenWrappedRejects() { $this->wrapped ->expects($this->once()) ->method('query') ->willReturn(Promise\reject(new \RuntimeException())); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $promise = $this->executor->query('8.8.8.8:53', $query); $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith(new \RuntimeException())); } public function testWrappedWillBeCancelledOnTimeout() { $this->executor = new TimeoutExecutor($this->wrapped, 0, $this->loop); $cancelled = 0; $this->wrapped ->expects($this->once()) ->method('query') ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { ++$cancelled; $reject(new CancellationException('Cancelled')); }); return $deferred->promise(); })); $callback = $this->expectCallableNever(); $errorback = $this->createCallableMock(); $errorback ->expects($this->once()) ->method('__invoke') ->with($this->logicalAnd( $this->isInstanceOf('React\Dns\Query\TimeoutException'), $this->attribute($this->equalTo('DNS query for igor.io timed out'), 'message') )); $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); $this->executor->query('8.8.8.8:53', $query)->then($callback, $errorback); $this->assertEquals(0, $cancelled); $this->loop->run(); $this->assertEquals(1, $cancelled); } } dns-0.4.16/tests/Query/UdpTransportExecutorTest.php000066400000000000000000000160571337201025100223750ustar00rootroot00000000000000getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); $dumper = $this->getMockBuilder('React\Dns\Protocol\BinaryDumper')->getMock(); $dumper->expects($this->once())->method('toBinary')->willReturn(str_repeat('.', 513)); $executor = new UdpTransportExecutor($loop, null, $dumper); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $promise = $executor->query('8.8.8.8:53', $query); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); $promise->then(null, $this->expectCallableOnce()); } public function testQueryRejectsIfServerConnectionFails() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->never())->method('addReadStream'); $executor = new UdpTransportExecutor($loop); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $promise = $executor->query('///', $query); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); $promise->then(null, $this->expectCallableOnce()); } /** * @group internet */ public function testQueryRejectsOnCancellation() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $loop->expects($this->once())->method('addReadStream'); $loop->expects($this->once())->method('removeReadStream'); $executor = new UdpTransportExecutor($loop); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $promise = $executor->query('8.8.8.8:53', $query); $promise->cancel(); $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); $promise->then(null, $this->expectCallableOnce()); } public function testQueryKeepsPendingIfServerRejectsNetworkPacket() { $loop = Factory::create(); $executor = new UdpTransportExecutor($loop); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $wait = true; $promise = $executor->query('127.0.0.1:1', $query)->then( null, function ($e) use (&$wait) { $wait = false; throw $e; } ); \Clue\React\Block\sleep(0.2, $loop); $this->assertTrue($wait); } public function testQueryKeepsPendingIfServerSendInvalidMessage() { $loop = Factory::create(); $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); $loop->addReadStream($server, function ($server) { $data = stream_socket_recvfrom($server, 512, 0, $peer); stream_socket_sendto($server, 'invalid', 0, $peer); }); $address = stream_socket_get_name($server, false); $executor = new UdpTransportExecutor($loop); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $wait = true; $promise = $executor->query($address, $query)->then( null, function ($e) use (&$wait) { $wait = false; throw $e; } ); \Clue\React\Block\sleep(0.2, $loop); $this->assertTrue($wait); } public function testQueryKeepsPendingIfServerSendInvalidId() { $parser = new Parser(); $dumper = new BinaryDumper(); $loop = Factory::create(); $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); $loop->addReadStream($server, function ($server) use ($parser, $dumper) { $data = stream_socket_recvfrom($server, 512, 0, $peer); $message = $parser->parseMessage($data); $message->header->set('id', 0); stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); }); $address = stream_socket_get_name($server, false); $executor = new UdpTransportExecutor($loop, $parser, $dumper); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $wait = true; $promise = $executor->query($address, $query)->then( null, function ($e) use (&$wait) { $wait = false; throw $e; } ); \Clue\React\Block\sleep(0.2, $loop); $this->assertTrue($wait); } public function testQueryRejectsIfServerSendsTruncatedResponse() { $parser = new Parser(); $dumper = new BinaryDumper(); $loop = Factory::create(); $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); $loop->addReadStream($server, function ($server) use ($parser, $dumper) { $data = stream_socket_recvfrom($server, 512, 0, $peer); $message = $parser->parseMessage($data); $message->header->set('tc', 1); stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); }); $address = stream_socket_get_name($server, false); $executor = new UdpTransportExecutor($loop, $parser, $dumper); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $wait = true; $promise = $executor->query($address, $query)->then( null, function ($e) use (&$wait) { $wait = false; throw $e; } ); // run loop for short period to ensure we detect connection ICMP rejection error \Clue\React\Block\sleep(0.01, $loop); if ($wait) { \Clue\React\Block\sleep(0.2, $loop); } $this->assertFalse($wait); } public function testQueryResolvesIfServerSendsValidResponse() { $parser = new Parser(); $dumper = new BinaryDumper(); $loop = Factory::create(); $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND); $loop->addReadStream($server, function ($server) use ($parser, $dumper) { $data = stream_socket_recvfrom($server, 512, 0, $peer); $message = $parser->parseMessage($data); stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); }); $address = stream_socket_get_name($server, false); $executor = new UdpTransportExecutor($loop, $parser, $dumper); $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); $promise = $executor->query($address, $query); $response = \Clue\React\Block\await($promise, $loop, 0.2); $this->assertInstanceOf('React\Dns\Model\Message', $response); } } dns-0.4.16/tests/Resolver/000077500000000000000000000000001337201025100153235ustar00rootroot00000000000000dns-0.4.16/tests/Resolver/FactoryTest.php000066400000000000000000000116231337201025100203060ustar00rootroot00000000000000getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $factory = new Factory(); $resolver = $factory->create('8.8.8.8:53', $loop); $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); } /** @test */ public function createWithoutPortShouldCreateResolverWithDefaultPort() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $factory = new Factory(); $resolver = $factory->create('8.8.8.8', $loop); $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); $this->assertSame('8.8.8.8:53', $this->getResolverPrivateMemberValue($resolver, 'nameserver')); } /** @test */ public function createCachedShouldCreateResolverWithCachedExecutor() { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $factory = new Factory(); $resolver = $factory->createCached('8.8.8.8:53', $loop); $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); $executor = $this->getResolverPrivateExecutor($resolver); $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); $this->assertInstanceOf('React\Cache\ArrayCache', $recordCacheCache); } /** @test */ public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCache() { $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $factory = new Factory(); $resolver = $factory->createCached('8.8.8.8:53', $loop, $cache); $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); $executor = $this->getResolverPrivateExecutor($resolver); $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); $this->assertSame($cache, $recordCacheCache); } /** * @test * @dataProvider factoryShouldAddDefaultPortProvider */ public function factoryShouldAddDefaultPort($input, $expected) { $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); $factory = new Factory(); $resolver = $factory->create($input, $loop); $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); $this->assertSame($expected, $this->getResolverPrivateMemberValue($resolver, 'nameserver')); } public static function factoryShouldAddDefaultPortProvider() { return array( array('8.8.8.8', '8.8.8.8:53'), array('1.2.3.4:5', '1.2.3.4:5'), array('localhost', 'localhost:53'), array('localhost:1234', 'localhost:1234'), array('::1', '[::1]:53'), array('[::1]:53', '[::1]:53') ); } private function getResolverPrivateExecutor($resolver) { $executor = $this->getResolverPrivateMemberValue($resolver, 'executor'); // extract underlying executor that may be wrapped in multiple layers of hosts file executors while ($executor instanceof HostsFileExecutor) { $reflector = new \ReflectionProperty('React\Dns\Query\HostsFileExecutor', 'fallback'); $reflector->setAccessible(true); $executor = $reflector->getValue($executor); } return $executor; } private function getResolverPrivateMemberValue($resolver, $field) { $reflector = new \ReflectionProperty('React\Dns\Resolver\Resolver', $field); $reflector->setAccessible(true); return $reflector->getValue($resolver); } private function getCachedExecutorPrivateMemberValue($resolver, $field) { $reflector = new \ReflectionProperty('React\Dns\Query\CachedExecutor', $field); $reflector->setAccessible(true); return $reflector->getValue($resolver); } private function getRecordCachePrivateMemberValue($resolver, $field) { $reflector = new \ReflectionProperty('React\Dns\Query\RecordCache', $field); $reflector->setAccessible(true); return $reflector->getValue($resolver); } } dns-0.4.16/tests/Resolver/ResolveAliasesTest.php000066400000000000000000000101731337201025100216170ustar00rootroot00000000000000createExecutorMock(); $resolver = new Resolver('8.8.8.8:53', $executor); $answers = $resolver->resolveAliases($answers, $name); $this->assertEquals($expectedAnswers, $answers); } public function provideAliasedAnswers() { return array( array( array('178.79.169.131'), array( new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), ), 'igor.io', ), array( array('178.79.169.131', '178.79.169.132', '178.79.169.133'), array( new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'), new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.133'), ), 'igor.io', ), array( array('178.79.169.131'), array( new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), ), 'igor.io', ), array( array(), array( new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN), new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN), ), 'igor.io', ), array( array('178.79.169.131'), array( new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'), new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), ), 'igor.io', ), array( array('178.79.169.131'), array( new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'), new Record('foo.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'bar.igor.io'), new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), ), 'igor.io', ), array( array('178.79.169.131', '178.79.169.132', '178.79.169.133'), array( new Record('igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'foo.igor.io'), new Record('foo.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'bar.igor.io'), new Record('bar.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'baz.igor.io'), new Record('bar.igor.io', Message::TYPE_CNAME, Message::CLASS_IN, 3600, 'qux.igor.io'), new Record('baz.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), new Record('baz.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'), new Record('qux.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.133'), ), 'igor.io', ), ); } private function createExecutorMock() { return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); } } dns-0.4.16/tests/Resolver/ResolverTest.php000066400000000000000000000242621337201025100205030ustar00rootroot00000000000000createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '178.79.169.131'); return Promise\resolve($response); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolve('igor.io')->then($this->expectCallableOnceWith('178.79.169.131')); } /** @test */ public function resolveAllShouldQueryGivenRecords() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1'); return Promise\resolve($response); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1'))); } /** @test */ public function resolveAllShouldIgnoreRecordsWithOtherTypes() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); $response->answers[] = new Record($query->name, Message::TYPE_TXT, $query->class, 3600, array('ignored')); $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1'); return Promise\resolve($response); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1'))); } /** @test */ public function resolveAllShouldReturnMultipleValuesForAlias() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); $response->answers[] = new Record($query->name, Message::TYPE_CNAME, $query->class, 3600, 'example.com'); $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::1'); $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::2'); $response->prepare(); return Promise\resolve($response); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then( $this->expectCallableOnceWith($this->equalTo(array('::1', '::2'))) ); } /** @test */ public function resolveShouldQueryARecordsAndIgnoreCase() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class); $response->answers[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class, 3600, '178.79.169.131'); return Promise\resolve($response); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolve('blog.wyrihaximus.net')->then($this->expectCallableOnceWith('178.79.169.131')); } /** @test */ public function resolveShouldFilterByName() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); $response->answers[] = new Record('foo.bar', $query->type, $query->class, 3600, '178.79.169.131'); return Promise\resolve($response); })); $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException')); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); } /** * @test */ public function resolveWithNoAnswersShouldCallErrbackIfGiven() { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) { $response = new Message(); $response->header->set('qr', 1); $response->questions[] = new Record($query->name, $query->type, $query->class); return Promise\resolve($response); })); $errback = $this->expectCallableOnceWith($this->callback(function ($param) { return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io did not return a valid answer (NOERROR / NODATA)'); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); } public function provideRcodeErrors() { return array( array( Message::RCODE_FORMAT_ERROR, 'DNS query for example.com returned an error response (Format Error)', ), array( Message::RCODE_SERVER_FAILURE, 'DNS query for example.com returned an error response (Server Failure)', ), array( Message::RCODE_NAME_ERROR, 'DNS query for example.com returned an error response (Non-Existent Domain / NXDOMAIN)' ), array( Message::RCODE_NOT_IMPLEMENTED, 'DNS query for example.com returned an error response (Not Implemented)' ), array( Message::RCODE_REFUSED, 'DNS query for example.com returned an error response (Refused)' ), array( 99, 'DNS query for example.com returned an error response (Unknown error response code 99)' ) ); } /** * @test * @dataProvider provideRcodeErrors */ public function resolveWithRcodeErrorShouldCallErrbackIfGiven($code, $expectedMessage) { $executor = $this->createExecutorMock(); $executor ->expects($this->once()) ->method('query') ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) ->will($this->returnCallback(function ($nameserver, $query) use ($code) { $response = new Message(); $response->header->set('qr', 1); $response->header->set('rcode', $code); $response->questions[] = new Record($query->name, $query->type, $query->class); return Promise\resolve($response); })); $errback = $this->expectCallableOnceWith($this->callback(function ($param) use ($code, $expectedMessage) { return ($param instanceof RecordNotFoundException && $param->getCode() === $code && $param->getMessage() === $expectedMessage); })); $resolver = new Resolver('8.8.8.8:53', $executor); $resolver->resolve('example.com')->then($this->expectCallableNever(), $errback); } public function testLegacyExtractAddress() { $executor = $this->createExecutorMock(); $resolver = new Resolver('8.8.8.8:53', $executor); $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); $response = Message::createResponseWithAnswersForQuery($query, array( new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3600, '1.2.3.4') )); $ret = $resolver->extractAddress($query, $response); $this->assertEquals('1.2.3.4', $ret); } private function createExecutorMock() { return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); } } dns-0.4.16/tests/TestCase.php000066400000000000000000000030571337201025100157530ustar00rootroot00000000000000createCallableMock(); $mock ->expects($this->once()) ->method('__invoke'); return $mock; } protected function expectCallableOnceWith($value) { $mock = $this->createCallableMock(); $mock ->expects($this->once()) ->method('__invoke') ->with($value); return $mock; } protected function expectCallableNever() { $mock = $this->createCallableMock(); $mock ->expects($this->never()) ->method('__invoke'); return $mock; } protected function createCallableMock() { return $this->getMockBuilder('React\Tests\Dns\CallableStub')->getMock(); } public function setExpectedException($exception, $exceptionMessage = '', $exceptionCode = null) { if (method_exists($this, 'expectException')) { // PHPUnit 5 $this->expectException($exception); if ($exceptionMessage !== '') { $this->expectExceptionMessage($exceptionMessage); } if ($exceptionCode !== null) { $this->expectExceptionCode($exceptionCode); } } else { // legacy PHPUnit 4 parent::setExpectedException($exception, $exceptionMessage, $exceptionCode); } } }