New upstream version 0.4.13
Dominik George
5 years ago
0 | language: php | |
1 | ||
2 | php: | |
3 | # - 5.3 # requires old distro, see below | |
4 | - 5.4 | |
5 | - 5.5 | |
6 | - 5.6 | |
7 | - 7.0 | |
8 | - 7.1 | |
9 | - 7.2 | |
10 | - hhvm # ignore errors, see below | |
11 | ||
12 | # lock distro so new future defaults will not break the build | |
13 | dist: trusty | |
14 | ||
15 | matrix: | |
16 | include: | |
17 | - php: 5.3 | |
18 | dist: precise | |
19 | allow_failures: | |
20 | - php: hhvm | |
21 | ||
22 | sudo: false | |
23 | ||
24 | install: | |
25 | - composer install --no-interaction | |
26 | ||
27 | script: | |
28 | - vendor/bin/phpunit --coverage-text |
0 | # Changelog | |
1 | ||
2 | ## 0.4.13 (2018-02-27) | |
3 | ||
4 | * Add `Config::loadSystemConfigBlocking()` to load default system config | |
5 | and support parsing DNS config on all supported platforms | |
6 | (`/etc/resolv.conf` on Unix/Linux/Mac and WMIC on Windows) | |
7 | (#92, #93, #94 and #95 by @clue) | |
8 | ||
9 | ```php | |
10 | $config = Config::loadSystemConfigBlocking(); | |
11 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
12 | ``` | |
13 | ||
14 | * Remove unneeded cyclic dependency on react/socket | |
15 | (#96 by @clue) | |
16 | ||
17 | ## 0.4.12 (2018-01-14) | |
18 | ||
19 | * Improve test suite by adding forward compatibility with PHPUnit 6, | |
20 | test against PHP 7.2, fix forward compatibility with upcoming EventLoop releases, | |
21 | add test group to skip integration tests relying on internet connection | |
22 | and add minor documentation improvements. | |
23 | (#85 and #87 by @carusogabriel, #88 and #89 by @clue and #83 by @jsor) | |
24 | ||
25 | ## 0.4.11 (2017-08-25) | |
26 | ||
27 | * Feature: Support resolving from default hosts file | |
28 | (#75, #76 and #77 by @clue) | |
29 | ||
30 | This means that resolving hosts such as `localhost` will now work as | |
31 | expected across all platforms with no changes required: | |
32 | ||
33 | ```php | |
34 | $resolver->resolve('localhost')->then(function ($ip) { | |
35 | echo 'IP: ' . $ip; | |
36 | }); | |
37 | ``` | |
38 | ||
39 | The new `HostsExecutor` exists for advanced usage and is otherwise used | |
40 | internally for this feature. | |
41 | ||
42 | ## 0.4.10 (2017-08-10) | |
43 | ||
44 | * Feature: Forward compatibility with EventLoop v1.0 and v0.5 and | |
45 | lock minimum dependencies and work around circular dependency for tests | |
46 | (#70 and #71 by @clue) | |
47 | ||
48 | * Fix: Work around DNS timeout issues for Windows users | |
49 | (#74 by @clue) | |
50 | ||
51 | * Documentation and examples for advanced usage | |
52 | (#66 by @WyriHaximus) | |
53 | ||
54 | * Remove broken TCP code, do not retry with invalid TCP query | |
55 | (#73 by @clue) | |
56 | ||
57 | * Improve test suite by fixing HHVM build for now again and ignore future HHVM build errors and | |
58 | lock Travis distro so new defaults will not break the build and | |
59 | fix failing tests for PHP 7.1 | |
60 | (#68 by @WyriHaximus and #69 and #72 by @clue) | |
61 | ||
62 | ## 0.4.9 (2017-05-01) | |
63 | ||
64 | * Feature: Forward compatibility with upcoming Socket v1.0 and v0.8 | |
65 | (#61 by @clue) | |
66 | ||
67 | ## 0.4.8 (2017-04-16) | |
68 | ||
69 | * Feature: Add support for the AAAA record type to the protocol parser | |
70 | (#58 by @othillo) | |
71 | ||
72 | * Feature: Add support for the PTR record type to the protocol parser | |
73 | (#59 by @othillo) | |
74 | ||
75 | ## 0.4.7 (2017-03-31) | |
76 | ||
77 | * Feature: Forward compatibility with upcoming Socket v0.6 and v0.7 component | |
78 | (#57 by @clue) | |
79 | ||
80 | ## 0.4.6 (2017-03-11) | |
81 | ||
82 | * Fix: Fix DNS timeout issues for Windows users and add forward compatibility | |
83 | with Stream v0.5 and upcoming v0.6 | |
84 | (#53 by @clue) | |
85 | ||
86 | * Improve test suite by adding PHPUnit to `require-dev` | |
87 | (#54 by @clue) | |
88 | ||
89 | ## 0.4.5 (2017-03-02) | |
90 | ||
91 | * Fix: Ensure we ignore the case of the answer | |
92 | (#51 by @WyriHaximus) | |
93 | ||
94 | * Feature: Add `TimeoutExecutor` and simplify internal APIs to allow internal | |
95 | code re-use for upcoming versions. | |
96 | (#48 and #49 by @clue) | |
97 | ||
98 | ## 0.4.4 (2017-02-13) | |
99 | ||
100 | * Fix: Fix handling connection and stream errors | |
101 | (#45 by @clue) | |
102 | ||
103 | * Feature: Add examples and forward compatibility with upcoming Socket v0.5 component | |
104 | (#46 and #47 by @clue) | |
105 | ||
106 | ## 0.4.3 (2016-07-31) | |
107 | ||
108 | * Feature: Allow for cache adapter injection (#38 by @WyriHaximus) | |
109 | ||
110 | ```php | |
111 | $factory = new React\Dns\Resolver\Factory(); | |
112 | ||
113 | $cache = new MyCustomCacheInstance(); | |
114 | $resolver = $factory->createCached('8.8.8.8', $loop, $cache); | |
115 | ``` | |
116 | ||
117 | * Feature: Support Promise cancellation (#35 by @clue) | |
118 | ||
119 | ```php | |
120 | $promise = $resolver->resolve('reactphp.org'); | |
121 | ||
122 | $promise->cancel(); | |
123 | ``` | |
124 | ||
125 | ## 0.4.2 (2016-02-24) | |
126 | ||
127 | * Repository maintenance, split off from main repo, improve test suite and documentation | |
128 | * First class support for PHP7 and HHVM (#34 by @clue) | |
129 | * Adjust compatibility to 5.3 (#30 by @clue) | |
130 | ||
131 | ## 0.4.1 (2014-04-13) | |
132 | ||
133 | * Bug fix: Fixed PSR-4 autoload path (@marcj/WyriHaximus) | |
134 | ||
135 | ## 0.4.0 (2014-02-02) | |
136 | ||
137 | * BC break: Bump minimum PHP version to PHP 5.4, remove 5.3 specific hacks | |
138 | * BC break: Update to React/Promise 2.0 | |
139 | * Bug fix: Properly resolve CNAME aliases | |
140 | * Dependency: Autoloading and filesystem structure now PSR-4 instead of PSR-0 | |
141 | * Bump React dependencies to v0.4 | |
142 | ||
143 | ## 0.3.2 (2013-05-10) | |
144 | ||
145 | * Feature: Support default port for IPv6 addresses (@clue) | |
146 | ||
147 | ## 0.3.0 (2013-04-14) | |
148 | ||
149 | * Bump React dependencies to v0.3 | |
150 | ||
151 | ## 0.2.6 (2012-12-26) | |
152 | ||
153 | * Feature: New cache component, used by DNS | |
154 | ||
155 | ## 0.2.5 (2012-11-26) | |
156 | ||
157 | * Version bump | |
158 | ||
159 | ## 0.2.4 (2012-11-18) | |
160 | ||
161 | * Feature: Change to promise-based API (@jsor) | |
162 | ||
163 | ## 0.2.3 (2012-11-14) | |
164 | ||
165 | * Version bump | |
166 | ||
167 | ## 0.2.2 (2012-10-28) | |
168 | ||
169 | * Feature: DNS executor timeout handling (@arnaud-lb) | |
170 | * Feature: DNS retry executor (@arnaud-lb) | |
171 | ||
172 | ## 0.2.1 (2012-10-14) | |
173 | ||
174 | * Minor adjustments to DNS parser | |
175 | ||
176 | ## 0.2.0 (2012-09-10) | |
177 | ||
178 | * Feature: DNS resolver |
0 | Copyright (c) 2012 Igor Wiedler, Chris Boden | |
1 | ||
2 | Permission is hereby granted, free of charge, to any person obtaining a copy | |
3 | of this software and associated documentation files (the "Software"), to deal | |
4 | in the Software without restriction, including without limitation the rights | |
5 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
6 | copies of the Software, and to permit persons to whom the Software is furnished | |
7 | to do so, subject to the following conditions: | |
8 | ||
9 | The above copyright notice and this permission notice shall be included in all | |
10 | copies or substantial portions of the Software. | |
11 | ||
12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
13 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
14 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
15 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
16 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
17 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |
18 | THE SOFTWARE. |
0 | # Dns | |
1 | ||
2 | [](https://travis-ci.org/reactphp/dns) | |
3 | ||
4 | Async DNS resolver for [ReactPHP](https://reactphp.org/). | |
5 | ||
6 | The main point of the DNS component is to provide async DNS resolution. | |
7 | However, it is really a toolkit for working with DNS messages, and could | |
8 | easily be used to create a DNS server. | |
9 | ||
10 | **Table of contents** | |
11 | ||
12 | * [Basic usage](#basic-usage) | |
13 | * [Caching](#caching) | |
14 | * [Custom cache adapter](#custom-cache-adapter) | |
15 | * [Advanced usage](#advanced-usage) | |
16 | * [HostsFileExecutor](#hostsfileexecutor) | |
17 | * [Install](#install) | |
18 | * [Tests](#tests) | |
19 | * [License](#license) | |
20 | * [References](#references) | |
21 | ||
22 | ## Basic usage | |
23 | ||
24 | The most basic usage is to just create a resolver through the resolver | |
25 | factory. All you need to give it is a nameserver, then you can start resolving | |
26 | names, baby! | |
27 | ||
28 | ```php | |
29 | $loop = React\EventLoop\Factory::create(); | |
30 | ||
31 | $config = React\Dns\Config\Config::loadSystemConfigBlocking(); | |
32 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
33 | ||
34 | $factory = new React\Dns\Resolver\Factory(); | |
35 | $dns = $factory->create($server, $loop); | |
36 | ||
37 | $dns->resolve('igor.io')->then(function ($ip) { | |
38 | echo "Host: $ip\n"; | |
39 | }); | |
40 | ||
41 | $loop->run(); | |
42 | ``` | |
43 | ||
44 | See also the [first example](examples). | |
45 | ||
46 | The `Config` class can be used to load the system default config. This is an | |
47 | operation that may access the filesystem and block. Ideally, this method should | |
48 | thus be executed only once before the loop starts and not repeatedly while it is | |
49 | running. | |
50 | Note that this class may return an *empty* configuration if the system config | |
51 | can not be loaded. As such, you'll likely want to apply a default nameserver | |
52 | as above if none can be found. | |
53 | ||
54 | > Note that the factory loads the hosts file from the filesystem once when | |
55 | creating the resolver instance. | |
56 | Ideally, this method should thus be executed only once before the loop starts | |
57 | and not repeatedly while it is running. | |
58 | ||
59 | Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
60 | ||
61 | ```php | |
62 | $promise = $resolver->resolve('reactphp.org'); | |
63 | ||
64 | $promise->cancel(); | |
65 | ``` | |
66 | ||
67 | But there's more. | |
68 | ||
69 | ## Caching | |
70 | ||
71 | You can cache results by configuring the resolver to use a `CachedExecutor`: | |
72 | ||
73 | ```php | |
74 | $loop = React\EventLoop\Factory::create(); | |
75 | ||
76 | $config = React\Dns\Config\Config::loadSystemConfigBlocking(); | |
77 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
78 | ||
79 | $factory = new React\Dns\Resolver\Factory(); | |
80 | $dns = $factory->createCached($server, $loop); | |
81 | ||
82 | $dns->resolve('igor.io')->then(function ($ip) { | |
83 | echo "Host: $ip\n"; | |
84 | }); | |
85 | ||
86 | ... | |
87 | ||
88 | $dns->resolve('igor.io')->then(function ($ip) { | |
89 | echo "Host: $ip\n"; | |
90 | }); | |
91 | ||
92 | $loop->run(); | |
93 | ``` | |
94 | ||
95 | If the first call returns before the second, only one query will be executed. | |
96 | The second result will be served from an in memory cache. | |
97 | This is particularly useful for long running scripts where the same hostnames | |
98 | have to be looked up multiple times. | |
99 | ||
100 | See also the [third example](examples). | |
101 | ||
102 | ### Custom cache adapter | |
103 | ||
104 | By default, the above will use an in memory cache. | |
105 | ||
106 | You can also specify a custom cache implementing [`CacheInterface`](https://github.com/reactphp/cache) to handle the record cache instead: | |
107 | ||
108 | ```php | |
109 | $cache = new React\Cache\ArrayCache(); | |
110 | $loop = React\EventLoop\Factory::create(); | |
111 | $factory = new React\Dns\Resolver\Factory(); | |
112 | $dns = $factory->createCached('8.8.8.8', $loop, $cache); | |
113 | ``` | |
114 | ||
115 | See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). | |
116 | ||
117 | ## Advanced Usage | |
118 | ||
119 | For more advanced usages one can utilize the `React\Dns\Query\Executor` directly. | |
120 | The following example looks up the `IPv6` address for `igor.io`. | |
121 | ||
122 | ```php | |
123 | $loop = Factory::create(); | |
124 | ||
125 | $executor = new Executor($loop, new Parser(), new BinaryDumper(), null); | |
126 | ||
127 | $executor->query( | |
128 | '8.8.8.8:53', | |
129 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time()) | |
130 | )->done(function (Message $message) { | |
131 | foreach ($message->answers as $answer) { | |
132 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
133 | } | |
134 | }, 'printf'); | |
135 | ||
136 | $loop->run(); | |
137 | ||
138 | ``` | |
139 | ||
140 | See also the [fourth example](examples). | |
141 | ||
142 | ### HostsFileExecutor | |
143 | ||
144 | Note that the above `Executor` class always performs an actual DNS query. | |
145 | If you also want to take entries from your hosts file into account, you may | |
146 | use this code: | |
147 | ||
148 | ```php | |
149 | $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); | |
150 | ||
151 | $executor = new Executor($loop, new Parser(), new BinaryDumper(), null); | |
152 | $executor = new HostsFileExecutor($hosts, $executor); | |
153 | ||
154 | $executor->query( | |
155 | '8.8.8.8:53', | |
156 | new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time()) | |
157 | ); | |
158 | ``` | |
159 | ||
160 | ## Install | |
161 | ||
162 | The recommended way to install this library is [through Composer](https://getcomposer.org). | |
163 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) | |
164 | ||
165 | This will install the latest supported version: | |
166 | ||
167 | ```bash | |
168 | $ composer require react/dns:^0.4.13 | |
169 | ``` | |
170 | ||
171 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. | |
172 | ||
173 | This project aims to run on any platform and thus does not require any PHP | |
174 | extensions and supports running on legacy PHP 5.3 through current PHP 7+ and | |
175 | HHVM. | |
176 | It's *highly recommended to use PHP 7+* for this project. | |
177 | ||
178 | ## Tests | |
179 | ||
180 | To run the test suite, you first need to clone this repo and then install all | |
181 | dependencies [through Composer](https://getcomposer.org): | |
182 | ||
183 | ```bash | |
184 | $ composer install | |
185 | ``` | |
186 | ||
187 | To run the test suite, go to the project root and run: | |
188 | ||
189 | ```bash | |
190 | $ php vendor/bin/phpunit | |
191 | ``` | |
192 | ||
193 | The test suite also contains a number of functional integration tests that rely | |
194 | on a stable internet connection. | |
195 | If you do not want to run these, they can simply be skipped like this: | |
196 | ||
197 | ```bash | |
198 | $ php vendor/bin/phpunit --exclude-group internet | |
199 | ``` | |
200 | ||
201 | ## License | |
202 | ||
203 | MIT, see [LICENSE file](LICENSE). | |
204 | ||
205 | ## References | |
206 | ||
207 | * [RFC 1034](https://tools.ietf.org/html/rfc1034) Domain Names - Concepts and Facilities | |
208 | * [RFC 1035](https://tools.ietf.org/html/rfc1035) Domain Names - Implementation and Specification |
0 | { | |
1 | "name": "react/dns", | |
2 | "description": "Async DNS resolver for ReactPHP", | |
3 | "keywords": ["dns", "dns-resolver", "ReactPHP", "async"], | |
4 | "license": "MIT", | |
5 | "require": { | |
6 | "php": ">=5.3.0", | |
7 | "react/cache": "~0.4.0|~0.3.0", | |
8 | "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5", | |
9 | "react/promise": "^2.1 || ^1.2.1", | |
10 | "react/promise-timer": "^1.2", | |
11 | "react/stream": "^1.0 || ^0.7 || ^0.6 || ^0.5 || ^0.4.5" | |
12 | }, | |
13 | "require-dev": { | |
14 | "clue/block-react": "^1.2", | |
15 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" | |
16 | }, | |
17 | "autoload": { | |
18 | "psr-4": { "React\\Dns\\": "src" } | |
19 | }, | |
20 | "autoload-dev": { | |
21 | "psr-4": { "React\\Tests\\Dns\\": "tests" } | |
22 | } | |
23 | } |
0 | <?php | |
1 | ||
2 | use React\Dns\Config\Config; | |
3 | use React\Dns\Resolver\Factory; | |
4 | ||
5 | require __DIR__ . '/../vendor/autoload.php'; | |
6 | ||
7 | $loop = React\EventLoop\Factory::create(); | |
8 | ||
9 | $config = Config::loadSystemConfigBlocking(); | |
10 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
11 | ||
12 | $factory = new Factory(); | |
13 | $resolver = $factory->create($server, $loop); | |
14 | ||
15 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; | |
16 | ||
17 | $resolver->resolve($name)->then(function ($ip) use ($name) { | |
18 | echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; | |
19 | }, 'printf'); | |
20 | ||
21 | $loop->run(); |
0 | <?php | |
1 | ||
2 | use React\Dns\Config\Config; | |
3 | use React\Dns\Resolver\Factory; | |
4 | ||
5 | require __DIR__ . '/../vendor/autoload.php'; | |
6 | ||
7 | $loop = React\EventLoop\Factory::create(); | |
8 | ||
9 | $config = Config::loadSystemConfigBlocking(); | |
10 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
11 | ||
12 | $factory = new Factory(); | |
13 | $resolver = $factory->create($server, $loop); | |
14 | ||
15 | $names = array_slice($argv, 1); | |
16 | if (!$names) { | |
17 | $names = array('google.com', 'www.google.com', 'gmail.com'); | |
18 | } | |
19 | ||
20 | foreach ($names as $name) { | |
21 | $resolver->resolve($name)->then(function ($ip) use ($name) { | |
22 | echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; | |
23 | }, 'printf'); | |
24 | } | |
25 | ||
26 | $loop->run(); |
0 | <?php | |
1 | ||
2 | use React\Dns\Config\Config; | |
3 | use React\Dns\Resolver\Factory; | |
4 | ||
5 | require __DIR__ . '/../vendor/autoload.php'; | |
6 | ||
7 | $loop = React\EventLoop\Factory::create(); | |
8 | ||
9 | $config = Config::loadSystemConfigBlocking(); | |
10 | $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8'; | |
11 | ||
12 | $factory = new Factory(); | |
13 | $resolver = $factory->createCached($server, $loop); | |
14 | ||
15 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; | |
16 | ||
17 | $resolver->resolve($name)->then(function ($ip) use ($name) { | |
18 | echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; | |
19 | }, 'printf'); | |
20 | ||
21 | $loop->addTimer(1.0, function() use ($name, $resolver) { | |
22 | $resolver->resolve($name)->then(function ($ip) use ($name) { | |
23 | echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; | |
24 | }, 'printf'); | |
25 | }); | |
26 | ||
27 | $loop->addTimer(2.0, function() use ($name, $resolver) { | |
28 | $resolver->resolve($name)->then(function ($ip) use ($name) { | |
29 | echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; | |
30 | }, 'printf'); | |
31 | }); | |
32 | ||
33 | $loop->addTimer(3.0, function() use ($name, $resolver) { | |
34 | $resolver->resolve($name)->then(function ($ip) use ($name) { | |
35 | echo 'IP for ' . $name . ': ' . $ip . PHP_EOL; | |
36 | }, 'printf'); | |
37 | }); | |
38 | ||
39 | $loop->run(); |
0 | <?php | |
1 | ||
2 | use React\Dns\Model\Message; | |
3 | use React\Dns\Protocol\BinaryDumper; | |
4 | use React\Dns\Protocol\Parser; | |
5 | use React\Dns\Query\Executor; | |
6 | use React\Dns\Query\Query; | |
7 | use React\EventLoop\Factory; | |
8 | ||
9 | require __DIR__ . '/../vendor/autoload.php'; | |
10 | ||
11 | $loop = Factory::create(); | |
12 | ||
13 | $executor = new Executor($loop, new Parser(), new BinaryDumper(), null); | |
14 | ||
15 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; | |
16 | ||
17 | $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN, time()); | |
18 | $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time()); | |
19 | ||
20 | $executor->query('8.8.8.8:53', $ipv4Query)->done(function (Message $message) { | |
21 | foreach ($message->answers as $answer) { | |
22 | echo 'IPv4: ' . $answer->data . PHP_EOL; | |
23 | } | |
24 | }, 'printf'); | |
25 | $executor->query('8.8.8.8:53', $ipv6Query)->done(function (Message $message) { | |
26 | foreach ($message->answers as $answer) { | |
27 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
28 | } | |
29 | }, 'printf'); | |
30 | ||
31 | $loop->run(); |
0 | <?xml version="1.0" encoding="UTF-8"?> | |
1 | ||
2 | <phpunit backupGlobals="false" | |
3 | backupStaticAttributes="false" | |
4 | colors="true" | |
5 | convertErrorsToExceptions="true" | |
6 | convertNoticesToExceptions="true" | |
7 | convertWarningsToExceptions="true" | |
8 | processIsolation="false" | |
9 | stopOnFailure="false" | |
10 | syntaxCheck="false" | |
11 | bootstrap="vendor/autoload.php" | |
12 | > | |
13 | <testsuites> | |
14 | <testsuite name="React Test Suite"> | |
15 | <directory>./tests/</directory> | |
16 | </testsuite> | |
17 | </testsuites> | |
18 | ||
19 | <filter> | |
20 | <whitelist> | |
21 | <directory>./src/</directory> | |
22 | </whitelist> | |
23 | </filter> | |
24 | </phpunit> |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Config; | |
3 | ||
4 | use RuntimeException; | |
5 | ||
6 | class Config | |
7 | { | |
8 | /** | |
9 | * Loads the system DNS configuration | |
10 | * | |
11 | * Note that this method may block while loading its internal files and/or | |
12 | * commands and should thus be used with care! While this should be | |
13 | * relatively fast for most systems, it remains unknown if this may block | |
14 | * under certain circumstances. In particular, this method should only be | |
15 | * executed before the loop starts, not while it is running. | |
16 | * | |
17 | * Note that this method will try to access its files and/or commands and | |
18 | * try to parse its output. Currently, this will only parse valid nameserver | |
19 | * entries from its output and will ignore all other output without | |
20 | * complaining. | |
21 | * | |
22 | * Note that the previous section implies that this may return an empty | |
23 | * `Config` object if no valid nameserver entries can be found. | |
24 | * | |
25 | * @return self | |
26 | * @codeCoverageIgnore | |
27 | */ | |
28 | public static function loadSystemConfigBlocking() | |
29 | { | |
30 | // Use WMIC output on Windows | |
31 | if (DIRECTORY_SEPARATOR === '\\') { | |
32 | return self::loadWmicBlocking(); | |
33 | } | |
34 | ||
35 | // otherwise (try to) load from resolv.conf | |
36 | try { | |
37 | return self::loadResolvConfBlocking(); | |
38 | } catch (RuntimeException $ignored) { | |
39 | // return empty config if parsing fails (file not found) | |
40 | return new self(); | |
41 | } | |
42 | } | |
43 | ||
44 | /** | |
45 | * Loads a resolv.conf file (from the given path or default location) | |
46 | * | |
47 | * Note that this method blocks while loading the given path and should | |
48 | * thus be used with care! While this should be relatively fast for normal | |
49 | * resolv.conf files, this may be an issue if this file is located on a slow | |
50 | * device or contains an excessive number of entries. In particular, this | |
51 | * method should only be executed before the loop starts, not while it is | |
52 | * running. | |
53 | * | |
54 | * Note that this method will throw if the given file can not be loaded, | |
55 | * such as if it is not readable or does not exist. In particular, this file | |
56 | * is not available on Windows. | |
57 | * | |
58 | * Currently, this will only parse valid "nameserver X" lines from the | |
59 | * given file contents. Lines can be commented out with "#" and ";" and | |
60 | * invalid lines will be ignored without complaining. See also | |
61 | * `man resolv.conf` for more details. | |
62 | * | |
63 | * Note that the previous section implies that this may return an empty | |
64 | * `Config` object if no valid "nameserver X" lines can be found. See also | |
65 | * `man resolv.conf` which suggests that the DNS server on the localhost | |
66 | * should be used in this case. This is left up to higher level consumers | |
67 | * of this API. | |
68 | * | |
69 | * @param ?string $path (optional) path to resolv.conf file or null=load default location | |
70 | * @return self | |
71 | * @throws RuntimeException if the path can not be loaded (does not exist) | |
72 | */ | |
73 | public static function loadResolvConfBlocking($path = null) | |
74 | { | |
75 | if ($path === null) { | |
76 | $path = '/etc/resolv.conf'; | |
77 | } | |
78 | ||
79 | $contents = @file_get_contents($path); | |
80 | if ($contents === false) { | |
81 | throw new RuntimeException('Unable to load resolv.conf file "' . $path . '"'); | |
82 | } | |
83 | ||
84 | preg_match_all('/^nameserver\s+(\S+)\s*$/m', $contents, $matches); | |
85 | ||
86 | $config = new self(); | |
87 | $config->nameservers = $matches[1]; | |
88 | ||
89 | return $config; | |
90 | } | |
91 | ||
92 | /** | |
93 | * Loads the DNS configurations from Windows's WMIC (from the given command or default command) | |
94 | * | |
95 | * Note that this method blocks while loading the given command and should | |
96 | * thus be used with care! While this should be relatively fast for normal | |
97 | * WMIC commands, it remains unknown if this may block under certain | |
98 | * circumstances. In particular, this method should only be executed before | |
99 | * the loop starts, not while it is running. | |
100 | * | |
101 | * Note that this method will only try to execute the given command try to | |
102 | * parse its output, irrespective of whether this command exists. In | |
103 | * particular, this command is only available on Windows. Currently, this | |
104 | * will only parse valid nameserver entries from the command output and will | |
105 | * ignore all other output without complaining. | |
106 | * | |
107 | * Note that the previous section implies that this may return an empty | |
108 | * `Config` object if no valid nameserver entries can be found. | |
109 | * | |
110 | * @param ?string $command (advanced) should not be given (NULL) unless you know what you're doing | |
111 | * @return self | |
112 | * @link https://ss64.com/nt/wmic.html | |
113 | */ | |
114 | public static function loadWmicBlocking($command = null) | |
115 | { | |
116 | $contents = shell_exec($command === null ? 'wmic NICCONFIG get "DNSServerSearchOrder" /format:CSV' : $command); | |
117 | preg_match_all('/(?<=[{;,"])([\da-f.:]{4,})(?=[};,"])/i', $contents, $matches); | |
118 | ||
119 | $config = new self(); | |
120 | $config->nameservers = $matches[1]; | |
121 | ||
122 | return $config; | |
123 | } | |
124 | ||
125 | public $nameservers = array(); | |
126 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Config; | |
3 | ||
4 | use React\EventLoop\LoopInterface; | |
5 | use React\Promise; | |
6 | use React\Promise\Deferred; | |
7 | use React\Stream\ReadableResourceStream; | |
8 | use React\Stream\Stream; | |
9 | ||
10 | /** | |
11 | * @deprecated | |
12 | * @see Config see Config class instead. | |
13 | */ | |
14 | class FilesystemFactory | |
15 | { | |
16 | private $loop; | |
17 | ||
18 | public function __construct(LoopInterface $loop) | |
19 | { | |
20 | $this->loop = $loop; | |
21 | } | |
22 | ||
23 | public function create($filename) | |
24 | { | |
25 | return $this | |
26 | ->loadEtcResolvConf($filename) | |
27 | ->then(array($this, 'parseEtcResolvConf')); | |
28 | } | |
29 | ||
30 | /** | |
31 | * @param string $contents | |
32 | * @return Promise | |
33 | * @deprecated see Config instead | |
34 | */ | |
35 | public function parseEtcResolvConf($contents) | |
36 | { | |
37 | return Promise\resolve(Config::loadResolvConfBlocking( | |
38 | 'data://text/plain;base64,' . base64_encode($contents) | |
39 | )); | |
40 | } | |
41 | ||
42 | public function loadEtcResolvConf($filename) | |
43 | { | |
44 | if (!file_exists($filename)) { | |
45 | return Promise\reject(new \InvalidArgumentException("The filename for /etc/resolv.conf given does not exist: $filename")); | |
46 | } | |
47 | ||
48 | try { | |
49 | $deferred = new Deferred(); | |
50 | ||
51 | $fd = fopen($filename, 'r'); | |
52 | stream_set_blocking($fd, 0); | |
53 | ||
54 | $contents = ''; | |
55 | ||
56 | $stream = class_exists('React\Stream\ReadableResourceStream') ? new ReadableResourceStream($fd, $this->loop) : new Stream($fd, $this->loop); | |
57 | $stream->on('data', function ($data) use (&$contents) { | |
58 | $contents .= $data; | |
59 | }); | |
60 | $stream->on('end', function () use (&$contents, $deferred) { | |
61 | $deferred->resolve($contents); | |
62 | }); | |
63 | $stream->on('error', function ($error) use ($deferred) { | |
64 | $deferred->reject($error); | |
65 | }); | |
66 | ||
67 | return $deferred->promise(); | |
68 | } catch (\Exception $e) { | |
69 | return Promise\reject($e); | |
70 | } | |
71 | } | |
72 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Config; | |
3 | ||
4 | use RuntimeException; | |
5 | ||
6 | /** | |
7 | * Represents a static hosts file which maps hostnames to IPs | |
8 | * | |
9 | * Hosts files are used on most systems to avoid actually hitting the DNS for | |
10 | * certain common hostnames. | |
11 | * | |
12 | * Most notably, this file usually contains an entry to map "localhost" to the | |
13 | * local IP. Windows is a notable exception here, as Windows does not actually | |
14 | * include "localhost" in this file by default. To compensate for this, this | |
15 | * class may explicitly be wrapped in another HostsFile instance which | |
16 | * hard-codes these entries for Windows (see also Factory). | |
17 | * | |
18 | * This class mostly exists to abstract the parsing/extraction process so this | |
19 | * can be replaced with a faster alternative in the future. | |
20 | */ | |
21 | class HostsFile | |
22 | { | |
23 | /** | |
24 | * Returns the default path for the hosts file on this system | |
25 | * | |
26 | * @return string | |
27 | * @codeCoverageIgnore | |
28 | */ | |
29 | public static function getDefaultPath() | |
30 | { | |
31 | // use static path for all Unix-based systems | |
32 | if (DIRECTORY_SEPARATOR !== '\\') { | |
33 | return '/etc/hosts'; | |
34 | } | |
35 | ||
36 | // Windows actually stores the path in the registry under | |
37 | // \HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath | |
38 | $path = '%SystemRoot%\\system32\drivers\etc\hosts'; | |
39 | ||
40 | $base = getenv('SystemRoot'); | |
41 | if ($base === false) { | |
42 | $base = 'C:\\Windows'; | |
43 | } | |
44 | ||
45 | return str_replace('%SystemRoot%', $base, $path); | |
46 | } | |
47 | ||
48 | /** | |
49 | * Loads a hosts file (from the given path or default location) | |
50 | * | |
51 | * Note that this method blocks while loading the given path and should | |
52 | * thus be used with care! While this should be relatively fast for normal | |
53 | * hosts file, this may be an issue if this file is located on a slow device | |
54 | * or contains an excessive number of entries. In particular, this method | |
55 | * should only be executed before the loop starts, not while it is running. | |
56 | * | |
57 | * @param ?string $path (optional) path to hosts file or null=load default location | |
58 | * @return self | |
59 | * @throws RuntimeException if the path can not be loaded (does not exist) | |
60 | */ | |
61 | public static function loadFromPathBlocking($path = null) | |
62 | { | |
63 | if ($path === null) { | |
64 | $path = self::getDefaultPath(); | |
65 | } | |
66 | ||
67 | $contents = @file_get_contents($path); | |
68 | if ($contents === false) { | |
69 | throw new RuntimeException('Unable to load hosts file "' . $path . '"'); | |
70 | } | |
71 | ||
72 | return new self($contents); | |
73 | } | |
74 | ||
75 | /** | |
76 | * Instantiate new hosts file with the given hosts file contents | |
77 | * | |
78 | * @param string $contents | |
79 | */ | |
80 | public function __construct($contents) | |
81 | { | |
82 | // remove all comments from the contents | |
83 | $contents = preg_replace('/[ \t]*#.*/', '', strtolower($contents)); | |
84 | ||
85 | $this->contents = $contents; | |
86 | } | |
87 | ||
88 | /** | |
89 | * Returns all IPs for the given hostname | |
90 | * | |
91 | * @param string $name | |
92 | * @return string[] | |
93 | */ | |
94 | public function getIpsForHost($name) | |
95 | { | |
96 | $name = strtolower($name); | |
97 | ||
98 | $ips = array(); | |
99 | foreach (preg_split('/\r?\n/', $this->contents) as $line) { | |
100 | $parts = preg_split('/\s+/', $line); | |
101 | $ip = array_shift($parts); | |
102 | if ($parts && array_search($name, $parts) !== false) { | |
103 | // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) | |
104 | if (strpos($ip, ':') !== false && ($pos = strpos($ip, '%')) !== false) { | |
105 | $ip = substr($ip, 0, $pos); | |
106 | } | |
107 | ||
108 | if (@inet_pton($ip) !== false) { | |
109 | $ips[] = $ip; | |
110 | } | |
111 | } | |
112 | } | |
113 | ||
114 | return $ips; | |
115 | } | |
116 | ||
117 | /** | |
118 | * Returns all hostnames for the given IPv4 or IPv6 address | |
119 | * | |
120 | * @param string $ip | |
121 | * @return string[] | |
122 | */ | |
123 | public function getHostsForIp($ip) | |
124 | { | |
125 | // check binary representation of IP to avoid string case and short notation | |
126 | $ip = @inet_pton($ip); | |
127 | if ($ip === false) { | |
128 | return array(); | |
129 | } | |
130 | ||
131 | $names = array(); | |
132 | foreach (preg_split('/\r?\n/', $this->contents) as $line) { | |
133 | $parts = preg_split('/\s+/', $line, null, PREG_SPLIT_NO_EMPTY); | |
134 | $addr = array_shift($parts); | |
135 | ||
136 | // remove IPv6 zone ID (`fe80::1%lo0` => `fe80:1`) | |
137 | if (strpos($addr, ':') !== false && ($pos = strpos($addr, '%')) !== false) { | |
138 | $addr = substr($addr, 0, $pos); | |
139 | } | |
140 | ||
141 | if (@inet_pton($addr) === $ip) { | |
142 | foreach ($parts as $part) { | |
143 | $names[] = $part; | |
144 | } | |
145 | } | |
146 | } | |
147 | ||
148 | return $names; | |
149 | } | |
150 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Model; | |
3 | ||
4 | class HeaderBag | |
5 | { | |
6 | public $data = ''; | |
7 | ||
8 | public $attributes = array( | |
9 | 'qdCount' => 0, | |
10 | 'anCount' => 0, | |
11 | 'nsCount' => 0, | |
12 | 'arCount' => 0, | |
13 | 'qr' => 0, | |
14 | 'opcode' => Message::OPCODE_QUERY, | |
15 | 'aa' => 0, | |
16 | 'tc' => 0, | |
17 | 'rd' => 0, | |
18 | 'ra' => 0, | |
19 | 'z' => 0, | |
20 | 'rcode' => Message::RCODE_OK, | |
21 | ); | |
22 | ||
23 | public function get($name) | |
24 | { | |
25 | return isset($this->attributes[$name]) ? $this->attributes[$name] : null; | |
26 | } | |
27 | ||
28 | public function set($name, $value) | |
29 | { | |
30 | $this->attributes[$name] = $value; | |
31 | } | |
32 | ||
33 | public function isQuery() | |
34 | { | |
35 | return 0 === $this->attributes['qr']; | |
36 | } | |
37 | ||
38 | public function isResponse() | |
39 | { | |
40 | return 1 === $this->attributes['qr']; | |
41 | } | |
42 | ||
43 | public function isTruncated() | |
44 | { | |
45 | return 1 === $this->attributes['tc']; | |
46 | } | |
47 | ||
48 | public function populateCounts(Message $message) | |
49 | { | |
50 | $this->attributes['qdCount'] = count($message->questions); | |
51 | $this->attributes['anCount'] = count($message->answers); | |
52 | $this->attributes['nsCount'] = count($message->authority); | |
53 | $this->attributes['arCount'] = count($message->additional); | |
54 | } | |
55 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Model; | |
3 | ||
4 | use React\Dns\Query\Query; | |
5 | use React\Dns\Model\Record; | |
6 | ||
7 | class Message | |
8 | { | |
9 | const TYPE_A = 1; | |
10 | const TYPE_NS = 2; | |
11 | const TYPE_CNAME = 5; | |
12 | const TYPE_SOA = 6; | |
13 | const TYPE_PTR = 12; | |
14 | const TYPE_MX = 15; | |
15 | const TYPE_TXT = 16; | |
16 | const TYPE_AAAA = 28; | |
17 | ||
18 | const CLASS_IN = 1; | |
19 | ||
20 | const OPCODE_QUERY = 0; | |
21 | const OPCODE_IQUERY = 1; // inverse query | |
22 | const OPCODE_STATUS = 2; | |
23 | ||
24 | const RCODE_OK = 0; | |
25 | const RCODE_FORMAT_ERROR = 1; | |
26 | const RCODE_SERVER_FAILURE = 2; | |
27 | const RCODE_NAME_ERROR = 3; | |
28 | const RCODE_NOT_IMPLEMENTED = 4; | |
29 | const RCODE_REFUSED = 5; | |
30 | ||
31 | /** | |
32 | * Creates a new request message for the given query | |
33 | * | |
34 | * @param Query $query | |
35 | * @return self | |
36 | */ | |
37 | public static function createRequestForQuery(Query $query) | |
38 | { | |
39 | $request = new Message(); | |
40 | $request->header->set('id', self::generateId()); | |
41 | $request->header->set('rd', 1); | |
42 | $request->questions[] = (array) $query; | |
43 | $request->prepare(); | |
44 | ||
45 | return $request; | |
46 | } | |
47 | ||
48 | /** | |
49 | * Creates a new response message for the given query with the given answer records | |
50 | * | |
51 | * @param Query $query | |
52 | * @param Record[] $answers | |
53 | * @return self | |
54 | */ | |
55 | public static function createResponseWithAnswersForQuery(Query $query, array $answers) | |
56 | { | |
57 | $response = new Message(); | |
58 | $response->header->set('id', self::generateId()); | |
59 | $response->header->set('qr', 1); | |
60 | $response->header->set('opcode', Message::OPCODE_QUERY); | |
61 | $response->header->set('rd', 1); | |
62 | $response->header->set('rcode', Message::RCODE_OK); | |
63 | ||
64 | $response->questions[] = (array) $query; | |
65 | ||
66 | foreach ($answers as $record) { | |
67 | $response->answers[] = $record; | |
68 | } | |
69 | ||
70 | $response->prepare(); | |
71 | ||
72 | return $response; | |
73 | } | |
74 | ||
75 | private static function generateId() | |
76 | { | |
77 | return mt_rand(0, 0xffff); | |
78 | } | |
79 | ||
80 | public $data = ''; | |
81 | ||
82 | public $header; | |
83 | public $questions = array(); | |
84 | public $answers = array(); | |
85 | public $authority = array(); | |
86 | public $additional = array(); | |
87 | ||
88 | public $consumed = 0; | |
89 | ||
90 | public function __construct() | |
91 | { | |
92 | $this->header = new HeaderBag(); | |
93 | } | |
94 | ||
95 | public function prepare() | |
96 | { | |
97 | $this->header->populateCounts($this); | |
98 | } | |
99 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Model; | |
3 | ||
4 | class Record | |
5 | { | |
6 | public $name; | |
7 | public $type; | |
8 | public $class; | |
9 | public $ttl; | |
10 | public $data; | |
11 | ||
12 | public function __construct($name, $type, $class, $ttl = 0, $data = null) | |
13 | { | |
14 | $this->name = $name; | |
15 | $this->type = $type; | |
16 | $this->class = $class; | |
17 | $this->ttl = $ttl; | |
18 | $this->data = $data; | |
19 | } | |
20 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Protocol; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Model\HeaderBag; | |
6 | ||
7 | class BinaryDumper | |
8 | { | |
9 | public function toBinary(Message $message) | |
10 | { | |
11 | $data = ''; | |
12 | ||
13 | $data .= $this->headerToBinary($message->header); | |
14 | $data .= $this->questionToBinary($message->questions); | |
15 | ||
16 | return $data; | |
17 | } | |
18 | ||
19 | private function headerToBinary(HeaderBag $header) | |
20 | { | |
21 | $data = ''; | |
22 | ||
23 | $data .= pack('n', $header->get('id')); | |
24 | ||
25 | $flags = 0x00; | |
26 | $flags = ($flags << 1) | $header->get('qr'); | |
27 | $flags = ($flags << 4) | $header->get('opcode'); | |
28 | $flags = ($flags << 1) | $header->get('aa'); | |
29 | $flags = ($flags << 1) | $header->get('tc'); | |
30 | $flags = ($flags << 1) | $header->get('rd'); | |
31 | $flags = ($flags << 1) | $header->get('ra'); | |
32 | $flags = ($flags << 3) | $header->get('z'); | |
33 | $flags = ($flags << 4) | $header->get('rcode'); | |
34 | ||
35 | $data .= pack('n', $flags); | |
36 | ||
37 | $data .= pack('n', $header->get('qdCount')); | |
38 | $data .= pack('n', $header->get('anCount')); | |
39 | $data .= pack('n', $header->get('nsCount')); | |
40 | $data .= pack('n', $header->get('arCount')); | |
41 | ||
42 | return $data; | |
43 | } | |
44 | ||
45 | private function questionToBinary(array $questions) | |
46 | { | |
47 | $data = ''; | |
48 | ||
49 | foreach ($questions as $question) { | |
50 | $labels = explode('.', $question['name']); | |
51 | foreach ($labels as $label) { | |
52 | $data .= chr(strlen($label)).$label; | |
53 | } | |
54 | $data .= "\x00"; | |
55 | ||
56 | $data .= pack('n*', $question['type'], $question['class']); | |
57 | } | |
58 | ||
59 | return $data; | |
60 | } | |
61 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Protocol; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Model\Record; | |
6 | use InvalidArgumentException; | |
7 | ||
8 | /** | |
9 | * DNS protocol parser | |
10 | * | |
11 | * Obsolete and uncommon types and classes are not implemented. | |
12 | */ | |
13 | class Parser | |
14 | { | |
15 | /** | |
16 | * Parses the given raw binary message into a Message object | |
17 | * | |
18 | * @param string $data | |
19 | * @throws InvalidArgumentException | |
20 | * @return Message | |
21 | */ | |
22 | public function parseMessage($data) | |
23 | { | |
24 | $message = new Message(); | |
25 | if ($this->parse($data, $message) !== $message) { | |
26 | throw new InvalidArgumentException('Unable to parse binary message'); | |
27 | } | |
28 | ||
29 | return $message; | |
30 | } | |
31 | ||
32 | /** | |
33 | * @deprecated unused, exists for BC only | |
34 | */ | |
35 | public function parseChunk($data, Message $message) | |
36 | { | |
37 | return $this->parse($data, $message); | |
38 | } | |
39 | ||
40 | private function parse($data, Message $message) | |
41 | { | |
42 | $message->data .= $data; | |
43 | ||
44 | if (!$message->header->get('id')) { | |
45 | if (!$this->parseHeader($message)) { | |
46 | return; | |
47 | } | |
48 | } | |
49 | ||
50 | if ($message->header->get('qdCount') != count($message->questions)) { | |
51 | if (!$this->parseQuestion($message)) { | |
52 | return; | |
53 | } | |
54 | } | |
55 | ||
56 | if ($message->header->get('anCount') != count($message->answers)) { | |
57 | if (!$this->parseAnswer($message)) { | |
58 | return; | |
59 | } | |
60 | } | |
61 | ||
62 | return $message; | |
63 | } | |
64 | ||
65 | public function parseHeader(Message $message) | |
66 | { | |
67 | if (strlen($message->data) < 12) { | |
68 | return; | |
69 | } | |
70 | ||
71 | $header = substr($message->data, 0, 12); | |
72 | $message->consumed += 12; | |
73 | ||
74 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); | |
75 | ||
76 | $rcode = $fields & bindec('1111'); | |
77 | $z = ($fields >> 4) & bindec('111'); | |
78 | $ra = ($fields >> 7) & 1; | |
79 | $rd = ($fields >> 8) & 1; | |
80 | $tc = ($fields >> 9) & 1; | |
81 | $aa = ($fields >> 10) & 1; | |
82 | $opcode = ($fields >> 11) & bindec('1111'); | |
83 | $qr = ($fields >> 15) & 1; | |
84 | ||
85 | $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', | |
86 | 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); | |
87 | ||
88 | ||
89 | foreach ($vars as $name => $value) { | |
90 | $message->header->set($name, $value); | |
91 | } | |
92 | ||
93 | return $message; | |
94 | } | |
95 | ||
96 | public function parseQuestion(Message $message) | |
97 | { | |
98 | if (strlen($message->data) < 2) { | |
99 | return; | |
100 | } | |
101 | ||
102 | $consumed = $message->consumed; | |
103 | ||
104 | list($labels, $consumed) = $this->readLabels($message->data, $consumed); | |
105 | ||
106 | if (null === $labels) { | |
107 | return; | |
108 | } | |
109 | ||
110 | if (strlen($message->data) - $consumed < 4) { | |
111 | return; | |
112 | } | |
113 | ||
114 | list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); | |
115 | $consumed += 4; | |
116 | ||
117 | $message->consumed = $consumed; | |
118 | ||
119 | $message->questions[] = array( | |
120 | 'name' => implode('.', $labels), | |
121 | 'type' => $type, | |
122 | 'class' => $class, | |
123 | ); | |
124 | ||
125 | if ($message->header->get('qdCount') != count($message->questions)) { | |
126 | return $this->parseQuestion($message); | |
127 | } | |
128 | ||
129 | return $message; | |
130 | } | |
131 | ||
132 | public function parseAnswer(Message $message) | |
133 | { | |
134 | if (strlen($message->data) < 2) { | |
135 | return; | |
136 | } | |
137 | ||
138 | $consumed = $message->consumed; | |
139 | ||
140 | list($labels, $consumed) = $this->readLabels($message->data, $consumed); | |
141 | ||
142 | if (null === $labels) { | |
143 | return; | |
144 | } | |
145 | ||
146 | if (strlen($message->data) - $consumed < 10) { | |
147 | return; | |
148 | } | |
149 | ||
150 | list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); | |
151 | $consumed += 4; | |
152 | ||
153 | list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4))); | |
154 | $consumed += 4; | |
155 | ||
156 | list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2))); | |
157 | $consumed += 2; | |
158 | ||
159 | $rdata = null; | |
160 | ||
161 | if (Message::TYPE_A === $type || Message::TYPE_AAAA === $type) { | |
162 | $ip = substr($message->data, $consumed, $rdLength); | |
163 | $consumed += $rdLength; | |
164 | ||
165 | $rdata = inet_ntop($ip); | |
166 | } | |
167 | ||
168 | if (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type) { | |
169 | list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); | |
170 | ||
171 | $rdata = implode('.', $bodyLabels); | |
172 | } | |
173 | ||
174 | $message->consumed = $consumed; | |
175 | ||
176 | $name = implode('.', $labels); | |
177 | $ttl = $this->signedLongToUnsignedLong($ttl); | |
178 | $record = new Record($name, $type, $class, $ttl, $rdata); | |
179 | ||
180 | $message->answers[] = $record; | |
181 | ||
182 | if ($message->header->get('anCount') != count($message->answers)) { | |
183 | return $this->parseAnswer($message); | |
184 | } | |
185 | ||
186 | return $message; | |
187 | } | |
188 | ||
189 | private function readLabels($data, $consumed) | |
190 | { | |
191 | $labels = array(); | |
192 | ||
193 | while (true) { | |
194 | if ($this->isEndOfLabels($data, $consumed)) { | |
195 | $consumed += 1; | |
196 | break; | |
197 | } | |
198 | ||
199 | if ($this->isCompressedLabel($data, $consumed)) { | |
200 | list($newLabels, $consumed) = $this->getCompressedLabel($data, $consumed); | |
201 | $labels = array_merge($labels, $newLabels); | |
202 | break; | |
203 | } | |
204 | ||
205 | $length = ord(substr($data, $consumed, 1)); | |
206 | $consumed += 1; | |
207 | ||
208 | if (strlen($data) - $consumed < $length) { | |
209 | return array(null, null); | |
210 | } | |
211 | ||
212 | $labels[] = substr($data, $consumed, $length); | |
213 | $consumed += $length; | |
214 | } | |
215 | ||
216 | return array($labels, $consumed); | |
217 | } | |
218 | ||
219 | public function isEndOfLabels($data, $consumed) | |
220 | { | |
221 | $length = ord(substr($data, $consumed, 1)); | |
222 | return 0 === $length; | |
223 | } | |
224 | ||
225 | public function getCompressedLabel($data, $consumed) | |
226 | { | |
227 | list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); | |
228 | list($labels) = $this->readLabels($data, $nameOffset); | |
229 | ||
230 | return array($labels, $consumed); | |
231 | } | |
232 | ||
233 | public function isCompressedLabel($data, $consumed) | |
234 | { | |
235 | $mask = 0xc000; // 1100000000000000 | |
236 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); | |
237 | ||
238 | return (bool) ($peek & $mask); | |
239 | } | |
240 | ||
241 | public function getCompressedLabelOffset($data, $consumed) | |
242 | { | |
243 | $mask = 0x3fff; // 0011111111111111 | |
244 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); | |
245 | ||
246 | return array($peek & $mask, $consumed + 2); | |
247 | } | |
248 | ||
249 | public function signedLongToUnsignedLong($i) | |
250 | { | |
251 | return $i & 0x80000000 ? $i - 0xffffffff : $i; | |
252 | } | |
253 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | ||
6 | class CachedExecutor implements ExecutorInterface | |
7 | { | |
8 | private $executor; | |
9 | private $cache; | |
10 | ||
11 | public function __construct(ExecutorInterface $executor, RecordCache $cache) | |
12 | { | |
13 | $this->executor = $executor; | |
14 | $this->cache = $cache; | |
15 | } | |
16 | ||
17 | public function query($nameserver, Query $query) | |
18 | { | |
19 | $executor = $this->executor; | |
20 | $cache = $this->cache; | |
21 | ||
22 | return $this->cache | |
23 | ->lookup($query) | |
24 | ->then( | |
25 | function ($cachedRecords) use ($query) { | |
26 | return Message::createResponseWithAnswersForQuery($query, $cachedRecords); | |
27 | }, | |
28 | function () use ($executor, $cache, $nameserver, $query) { | |
29 | return $executor | |
30 | ->query($nameserver, $query) | |
31 | ->then(function ($response) use ($cache, $query) { | |
32 | $cache->storeResponseMessage($query->currentTime, $response); | |
33 | return $response; | |
34 | }); | |
35 | } | |
36 | ); | |
37 | } | |
38 | ||
39 | /** | |
40 | * @deprecated unused, exists for BC only | |
41 | */ | |
42 | public function buildResponse(Query $query, array $cachedRecords) | |
43 | { | |
44 | return Message::createResponseWithAnswersForQuery($query, $cachedRecords); | |
45 | } | |
46 | ||
47 | /** | |
48 | * @deprecated unused, exists for BC only | |
49 | */ | |
50 | protected function generateId() | |
51 | { | |
52 | return mt_rand(0, 0xffff); | |
53 | } | |
54 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | class CancellationException extends \RuntimeException | |
5 | { | |
6 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Protocol\Parser; | |
6 | use React\Dns\Protocol\BinaryDumper; | |
7 | use React\EventLoop\LoopInterface; | |
8 | use React\Promise\Deferred; | |
9 | use React\Promise; | |
10 | use React\Stream\DuplexResourceStream; | |
11 | use React\Stream\Stream; | |
12 | ||
13 | class Executor implements ExecutorInterface | |
14 | { | |
15 | private $loop; | |
16 | private $parser; | |
17 | private $dumper; | |
18 | private $timeout; | |
19 | ||
20 | /** | |
21 | * | |
22 | * Note that albeit supported, the $timeout parameter is deprecated! | |
23 | * You should pass a `null` value here instead. If you need timeout handling, | |
24 | * use the `TimeoutConnector` instead. | |
25 | * | |
26 | * @param LoopInterface $loop | |
27 | * @param Parser $parser | |
28 | * @param BinaryDumper $dumper | |
29 | * @param null|float $timeout DEPRECATED: timeout for DNS query or NULL=no timeout | |
30 | */ | |
31 | public function __construct(LoopInterface $loop, Parser $parser, BinaryDumper $dumper, $timeout = 5) | |
32 | { | |
33 | $this->loop = $loop; | |
34 | $this->parser = $parser; | |
35 | $this->dumper = $dumper; | |
36 | $this->timeout = $timeout; | |
37 | } | |
38 | ||
39 | public function query($nameserver, Query $query) | |
40 | { | |
41 | $request = Message::createRequestForQuery($query); | |
42 | ||
43 | $queryData = $this->dumper->toBinary($request); | |
44 | $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; | |
45 | ||
46 | return $this->doQuery($nameserver, $transport, $queryData, $query->name); | |
47 | } | |
48 | ||
49 | /** | |
50 | * @deprecated unused, exists for BC only | |
51 | */ | |
52 | public function prepareRequest(Query $query) | |
53 | { | |
54 | return Message::createRequestForQuery($query); | |
55 | } | |
56 | ||
57 | public function doQuery($nameserver, $transport, $queryData, $name) | |
58 | { | |
59 | // we only support UDP right now | |
60 | if ($transport !== 'udp') { | |
61 | return Promise\reject(new \RuntimeException( | |
62 | 'DNS query for ' . $name . ' failed: Requested transport "' . $transport . '" not available, only UDP is supported in this version' | |
63 | )); | |
64 | } | |
65 | ||
66 | $that = $this; | |
67 | $parser = $this->parser; | |
68 | $loop = $this->loop; | |
69 | ||
70 | // UDP connections are instant, so try this without a timer | |
71 | try { | |
72 | $conn = $this->createConnection($nameserver, $transport); | |
73 | } catch (\Exception $e) { | |
74 | return Promise\reject(new \RuntimeException('DNS query for ' . $name . ' failed: ' . $e->getMessage(), 0, $e)); | |
75 | } | |
76 | ||
77 | $deferred = new Deferred(function ($resolve, $reject) use (&$timer, $loop, &$conn, $name) { | |
78 | $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name))); | |
79 | ||
80 | if ($timer !== null) { | |
81 | $loop->cancelTimer($timer); | |
82 | } | |
83 | $conn->close(); | |
84 | }); | |
85 | ||
86 | $timer = null; | |
87 | if ($this->timeout !== null) { | |
88 | $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { | |
89 | $conn->close(); | |
90 | $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); | |
91 | }); | |
92 | } | |
93 | ||
94 | $conn->on('data', function ($data) use ($conn, $parser, $deferred, $timer, $loop, $name) { | |
95 | $conn->end(); | |
96 | if ($timer !== null) { | |
97 | $loop->cancelTimer($timer); | |
98 | } | |
99 | ||
100 | try { | |
101 | $response = $parser->parseMessage($data); | |
102 | } catch (\Exception $e) { | |
103 | $deferred->reject($e); | |
104 | return; | |
105 | } | |
106 | ||
107 | if ($response->header->isTruncated()) { | |
108 | $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')); | |
109 | return; | |
110 | } | |
111 | ||
112 | $deferred->resolve($response); | |
113 | }); | |
114 | $conn->write($queryData); | |
115 | ||
116 | return $deferred->promise(); | |
117 | } | |
118 | ||
119 | /** | |
120 | * @deprecated unused, exists for BC only | |
121 | */ | |
122 | protected function generateId() | |
123 | { | |
124 | return mt_rand(0, 0xffff); | |
125 | } | |
126 | ||
127 | /** | |
128 | * @param string $nameserver | |
129 | * @param string $transport | |
130 | * @return \React\Stream\DuplexStreamInterface | |
131 | */ | |
132 | protected function createConnection($nameserver, $transport) | |
133 | { | |
134 | $fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); | |
135 | if ($fd === false) { | |
136 | throw new \RuntimeException('Unable to connect to DNS server: ' . $errstr, $errno); | |
137 | } | |
138 | ||
139 | // Instantiate stream instance around this stream resource. | |
140 | // This ought to be replaced with a datagram socket in the future. | |
141 | // Temporary work around for Windows 10: buffer whole UDP response | |
142 | // @coverageIgnoreStart | |
143 | if (!class_exists('React\Stream\Stream')) { | |
144 | // prefer DuplexResourceStream as of react/stream v0.7.0 | |
145 | $conn = new DuplexResourceStream($fd, $this->loop, -1); | |
146 | } else { | |
147 | // use legacy Stream class for react/stream < v0.7.0 | |
148 | $conn = new Stream($fd, $this->loop); | |
149 | $conn->bufferSize = null; | |
150 | } | |
151 | // @coverageIgnoreEnd | |
152 | ||
153 | return $conn; | |
154 | } | |
155 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | interface ExecutorInterface | |
5 | { | |
6 | public function query($nameserver, Query $query); | |
7 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Config\HostsFile; | |
5 | use React\Dns\Model\Message; | |
6 | use React\Dns\Model\Record; | |
7 | use React\Promise; | |
8 | ||
9 | /** | |
10 | * Resolves hosts from the givne HostsFile or falls back to another executor | |
11 | * | |
12 | * If the host is found in the hosts file, it will not be passed to the actual | |
13 | * DNS executor. If the host is not found in the hosts file, it will be passed | |
14 | * to the DNS executor as a fallback. | |
15 | */ | |
16 | class HostsFileExecutor implements ExecutorInterface | |
17 | { | |
18 | private $hosts; | |
19 | private $fallback; | |
20 | ||
21 | public function __construct(HostsFile $hosts, ExecutorInterface $fallback) | |
22 | { | |
23 | $this->hosts = $hosts; | |
24 | $this->fallback = $fallback; | |
25 | } | |
26 | ||
27 | public function query($nameserver, Query $query) | |
28 | { | |
29 | if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) { | |
30 | // forward lookup for type A or AAAA | |
31 | $records = array(); | |
32 | $expectsColon = $query->type === Message::TYPE_AAAA; | |
33 | foreach ($this->hosts->getIpsForHost($query->name) as $ip) { | |
34 | // ensure this is an IPv4/IPV6 address according to query type | |
35 | if ((strpos($ip, ':') !== false) === $expectsColon) { | |
36 | $records[] = new Record($query->name, $query->type, $query->class, 0, $ip); | |
37 | } | |
38 | } | |
39 | ||
40 | if ($records) { | |
41 | return Promise\resolve( | |
42 | Message::createResponseWithAnswersForQuery($query, $records) | |
43 | ); | |
44 | } | |
45 | } elseif ($query->class === Message::CLASS_IN && $query->type === Message::TYPE_PTR) { | |
46 | // reverse lookup: extract IPv4 or IPv6 from special `.arpa` domain | |
47 | $ip = $this->getIpFromHost($query->name); | |
48 | ||
49 | if ($ip !== null) { | |
50 | $records = array(); | |
51 | foreach ($this->hosts->getHostsForIp($ip) as $host) { | |
52 | $records[] = new Record($query->name, $query->type, $query->class, 0, $host); | |
53 | } | |
54 | ||
55 | if ($records) { | |
56 | return Promise\resolve( | |
57 | Message::createResponseWithAnswersForQuery($query, $records) | |
58 | ); | |
59 | } | |
60 | } | |
61 | } | |
62 | ||
63 | return $this->fallback->query($nameserver, $query); | |
64 | } | |
65 | ||
66 | private function getIpFromHost($host) | |
67 | { | |
68 | if (substr($host, -13) === '.in-addr.arpa') { | |
69 | // IPv4: read as IP and reverse bytes | |
70 | $ip = @inet_pton(substr($host, 0, -13)); | |
71 | if ($ip === false || isset($ip[4])) { | |
72 | return null; | |
73 | } | |
74 | ||
75 | return inet_ntop(strrev($ip)); | |
76 | } elseif (substr($host, -9) === '.ip6.arpa') { | |
77 | // IPv6: replace dots, reverse nibbles and interpret as hexadecimal string | |
78 | $ip = @inet_ntop(pack('H*', strrev(str_replace('.', '', substr($host, 0, -9))))); | |
79 | if ($ip === false) { | |
80 | return null; | |
81 | } | |
82 | ||
83 | return $ip; | |
84 | } else { | |
85 | return null; | |
86 | } | |
87 | } | |
88 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | class Query | |
5 | { | |
6 | public $name; | |
7 | public $type; | |
8 | public $class; | |
9 | public $currentTime; | |
10 | ||
11 | public function __construct($name, $type, $class, $currentTime) | |
12 | { | |
13 | $this->name = $name; | |
14 | $this->type = $type; | |
15 | $this->class = $class; | |
16 | $this->currentTime = $currentTime; | |
17 | } | |
18 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Model\Record; | |
6 | ||
7 | class RecordBag | |
8 | { | |
9 | private $records = array(); | |
10 | ||
11 | public function set($currentTime, Record $record) | |
12 | { | |
13 | $this->records[$record->data] = array($currentTime + $record->ttl, $record); | |
14 | } | |
15 | ||
16 | public function all() | |
17 | { | |
18 | return array_values(array_map( | |
19 | function ($value) { | |
20 | list($expiresAt, $record) = $value; | |
21 | return $record; | |
22 | }, | |
23 | $this->records | |
24 | )); | |
25 | } | |
26 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Cache\CacheInterface; | |
5 | use React\Dns\Model\Message; | |
6 | use React\Dns\Model\Record; | |
7 | use React\Promise; | |
8 | ||
9 | class RecordCache | |
10 | { | |
11 | private $cache; | |
12 | private $expiredAt; | |
13 | ||
14 | public function __construct(CacheInterface $cache) | |
15 | { | |
16 | $this->cache = $cache; | |
17 | } | |
18 | ||
19 | public function lookup(Query $query) | |
20 | { | |
21 | $id = $this->serializeQueryToIdentity($query); | |
22 | ||
23 | $expiredAt = $this->expiredAt; | |
24 | ||
25 | return $this->cache | |
26 | ->get($id) | |
27 | ->then(function ($value) use ($query, $expiredAt) { | |
28 | $recordBag = unserialize($value); | |
29 | ||
30 | if (null !== $expiredAt && $expiredAt <= $query->currentTime) { | |
31 | return Promise\reject(); | |
32 | } | |
33 | ||
34 | return $recordBag->all(); | |
35 | }); | |
36 | } | |
37 | ||
38 | public function storeResponseMessage($currentTime, Message $message) | |
39 | { | |
40 | foreach ($message->answers as $record) { | |
41 | $this->storeRecord($currentTime, $record); | |
42 | } | |
43 | } | |
44 | ||
45 | public function storeRecord($currentTime, Record $record) | |
46 | { | |
47 | $id = $this->serializeRecordToIdentity($record); | |
48 | ||
49 | $cache = $this->cache; | |
50 | ||
51 | $this->cache | |
52 | ->get($id) | |
53 | ->then( | |
54 | function ($value) { | |
55 | return unserialize($value); | |
56 | }, | |
57 | function ($e) { | |
58 | return new RecordBag(); | |
59 | } | |
60 | ) | |
61 | ->then(function ($recordBag) use ($id, $currentTime, $record, $cache) { | |
62 | $recordBag->set($currentTime, $record); | |
63 | $cache->set($id, serialize($recordBag)); | |
64 | }); | |
65 | } | |
66 | ||
67 | public function expire($currentTime) | |
68 | { | |
69 | $this->expiredAt = $currentTime; | |
70 | } | |
71 | ||
72 | public function serializeQueryToIdentity(Query $query) | |
73 | { | |
74 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); | |
75 | } | |
76 | ||
77 | public function serializeRecordToIdentity(Record $record) | |
78 | { | |
79 | return sprintf('%s:%s:%s', $record->name, $record->type, $record->class); | |
80 | } | |
81 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Promise\Deferred; | |
5 | ||
6 | class RetryExecutor implements ExecutorInterface | |
7 | { | |
8 | private $executor; | |
9 | private $retries; | |
10 | ||
11 | public function __construct(ExecutorInterface $executor, $retries = 2) | |
12 | { | |
13 | $this->executor = $executor; | |
14 | $this->retries = $retries; | |
15 | } | |
16 | ||
17 | public function query($nameserver, Query $query) | |
18 | { | |
19 | return $this->tryQuery($nameserver, $query, $this->retries); | |
20 | } | |
21 | ||
22 | public function tryQuery($nameserver, Query $query, $retries) | |
23 | { | |
24 | $that = $this; | |
25 | $errorback = function ($error) use ($nameserver, $query, $retries, $that) { | |
26 | if (!$error instanceof TimeoutException) { | |
27 | throw $error; | |
28 | } | |
29 | if (0 >= $retries) { | |
30 | throw new \RuntimeException( | |
31 | sprintf("DNS query for %s failed: too many retries", $query->name), | |
32 | 0, | |
33 | $error | |
34 | ); | |
35 | } | |
36 | return $that->tryQuery($nameserver, $query, $retries-1); | |
37 | }; | |
38 | ||
39 | return $this->executor | |
40 | ->query($nameserver, $query) | |
41 | ->then(null, $errorback); | |
42 | } | |
43 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\EventLoop\LoopInterface; | |
5 | use React\Promise\Deferred; | |
6 | use React\Promise\CancellablePromiseInterface; | |
7 | use React\Promise\Timer; | |
8 | ||
9 | class TimeoutExecutor implements ExecutorInterface | |
10 | { | |
11 | private $executor; | |
12 | private $loop; | |
13 | private $timeout; | |
14 | ||
15 | public function __construct(ExecutorInterface $executor, $timeout, LoopInterface $loop) | |
16 | { | |
17 | $this->executor = $executor; | |
18 | $this->loop = $loop; | |
19 | $this->timeout = $timeout; | |
20 | } | |
21 | ||
22 | public function query($nameserver, Query $query) | |
23 | { | |
24 | return Timer\timeout($this->executor->query($nameserver, $query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { | |
25 | if ($e instanceof Timer\TimeoutException) { | |
26 | $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e); | |
27 | } | |
28 | throw $e; | |
29 | }); | |
30 | } | |
31 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Resolver; | |
3 | ||
4 | use React\Cache\ArrayCache; | |
5 | use React\Cache\CacheInterface; | |
6 | use React\Dns\Config\HostsFile; | |
7 | use React\Dns\Protocol\Parser; | |
8 | use React\Dns\Protocol\BinaryDumper; | |
9 | use React\Dns\Query\CachedExecutor; | |
10 | use React\Dns\Query\Executor; | |
11 | use React\Dns\Query\ExecutorInterface; | |
12 | use React\Dns\Query\HostsFileExecutor; | |
13 | use React\Dns\Query\RecordCache; | |
14 | use React\Dns\Query\RetryExecutor; | |
15 | use React\Dns\Query\TimeoutExecutor; | |
16 | use React\EventLoop\LoopInterface; | |
17 | ||
18 | class Factory | |
19 | { | |
20 | public function create($nameserver, LoopInterface $loop) | |
21 | { | |
22 | $nameserver = $this->addPortToServerIfMissing($nameserver); | |
23 | $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop)); | |
24 | ||
25 | return new Resolver($nameserver, $executor); | |
26 | } | |
27 | ||
28 | public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null) | |
29 | { | |
30 | if (!($cache instanceof CacheInterface)) { | |
31 | $cache = new ArrayCache(); | |
32 | } | |
33 | ||
34 | $nameserver = $this->addPortToServerIfMissing($nameserver); | |
35 | $executor = $this->decorateHostsFileExecutor($this->createCachedExecutor($loop, $cache)); | |
36 | ||
37 | return new Resolver($nameserver, $executor); | |
38 | } | |
39 | ||
40 | /** | |
41 | * Tries to load the hosts file and decorates the given executor on success | |
42 | * | |
43 | * @param ExecutorInterface $executor | |
44 | * @return ExecutorInterface | |
45 | * @codeCoverageIgnore | |
46 | */ | |
47 | private function decorateHostsFileExecutor(ExecutorInterface $executor) | |
48 | { | |
49 | try { | |
50 | $executor = new HostsFileExecutor( | |
51 | HostsFile::loadFromPathBlocking(), | |
52 | $executor | |
53 | ); | |
54 | } catch (\RuntimeException $e) { | |
55 | // ignore this file if it can not be loaded | |
56 | } | |
57 | ||
58 | // Windows does not store localhost in hosts file by default but handles this internally | |
59 | // To compensate for this, we explicitly use hard-coded defaults for localhost | |
60 | if (DIRECTORY_SEPARATOR === '\\') { | |
61 | $executor = new HostsFileExecutor( | |
62 | new HostsFile("127.0.0.1 localhost\n::1 localhost"), | |
63 | $executor | |
64 | ); | |
65 | } | |
66 | ||
67 | return $executor; | |
68 | } | |
69 | ||
70 | protected function createExecutor(LoopInterface $loop) | |
71 | { | |
72 | return new TimeoutExecutor( | |
73 | new Executor($loop, new Parser(), new BinaryDumper(), null), | |
74 | 5.0, | |
75 | $loop | |
76 | ); | |
77 | } | |
78 | ||
79 | protected function createRetryExecutor(LoopInterface $loop) | |
80 | { | |
81 | return new RetryExecutor($this->createExecutor($loop)); | |
82 | } | |
83 | ||
84 | protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache) | |
85 | { | |
86 | return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache)); | |
87 | } | |
88 | ||
89 | protected function addPortToServerIfMissing($nameserver) | |
90 | { | |
91 | if (strpos($nameserver, '[') === false && substr_count($nameserver, ':') >= 2) { | |
92 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
93 | $nameserver = '[' . $nameserver . ']'; | |
94 | } | |
95 | // assume a dummy scheme when checking for the port, otherwise parse_url() fails | |
96 | if (parse_url('dummy://' . $nameserver, PHP_URL_PORT) === null) { | |
97 | $nameserver .= ':53'; | |
98 | } | |
99 | ||
100 | return $nameserver; | |
101 | } | |
102 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Resolver; | |
3 | ||
4 | use React\Dns\Query\ExecutorInterface; | |
5 | use React\Dns\Query\Query; | |
6 | use React\Dns\RecordNotFoundException; | |
7 | use React\Dns\Model\Message; | |
8 | ||
9 | class Resolver | |
10 | { | |
11 | private $nameserver; | |
12 | private $executor; | |
13 | ||
14 | public function __construct($nameserver, ExecutorInterface $executor) | |
15 | { | |
16 | $this->nameserver = $nameserver; | |
17 | $this->executor = $executor; | |
18 | } | |
19 | ||
20 | public function resolve($domain) | |
21 | { | |
22 | $query = new Query($domain, Message::TYPE_A, Message::CLASS_IN, time()); | |
23 | $that = $this; | |
24 | ||
25 | return $this->executor | |
26 | ->query($this->nameserver, $query) | |
27 | ->then(function (Message $response) use ($query, $that) { | |
28 | return $that->extractAddress($query, $response); | |
29 | }); | |
30 | } | |
31 | ||
32 | public function extractAddress(Query $query, Message $response) | |
33 | { | |
34 | $answers = $response->answers; | |
35 | ||
36 | $addresses = $this->resolveAliases($answers, $query->name); | |
37 | ||
38 | if (0 === count($addresses)) { | |
39 | $message = 'DNS Request did not return valid answer.'; | |
40 | throw new RecordNotFoundException($message); | |
41 | } | |
42 | ||
43 | $address = $addresses[array_rand($addresses)]; | |
44 | return $address; | |
45 | } | |
46 | ||
47 | public function resolveAliases(array $answers, $name) | |
48 | { | |
49 | $named = $this->filterByName($answers, $name); | |
50 | $aRecords = $this->filterByType($named, Message::TYPE_A); | |
51 | $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME); | |
52 | ||
53 | if ($aRecords) { | |
54 | return $this->mapRecordData($aRecords); | |
55 | } | |
56 | ||
57 | if ($cnameRecords) { | |
58 | $aRecords = array(); | |
59 | ||
60 | $cnames = $this->mapRecordData($cnameRecords); | |
61 | foreach ($cnames as $cname) { | |
62 | $targets = $this->filterByName($answers, $cname); | |
63 | $aRecords = array_merge( | |
64 | $aRecords, | |
65 | $this->resolveAliases($answers, $cname) | |
66 | ); | |
67 | } | |
68 | ||
69 | return $aRecords; | |
70 | } | |
71 | ||
72 | return array(); | |
73 | } | |
74 | ||
75 | private function filterByName(array $answers, $name) | |
76 | { | |
77 | return $this->filterByField($answers, 'name', $name); | |
78 | } | |
79 | ||
80 | private function filterByType(array $answers, $type) | |
81 | { | |
82 | return $this->filterByField($answers, 'type', $type); | |
83 | } | |
84 | ||
85 | private function filterByField(array $answers, $field, $value) | |
86 | { | |
87 | $value = strtolower($value); | |
88 | return array_filter($answers, function ($answer) use ($field, $value) { | |
89 | return $value === strtolower($answer->$field); | |
90 | }); | |
91 | } | |
92 | ||
93 | private function mapRecordData(array $records) | |
94 | { | |
95 | return array_map(function ($record) { | |
96 | return $record->data; | |
97 | }, $records); | |
98 | } | |
99 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns; | |
3 | ||
4 | class CallableStub | |
5 | { | |
6 | public function __invoke() | |
7 | { | |
8 | } | |
9 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Config; | |
3 | ||
4 | use React\Tests\Dns\TestCase; | |
5 | use React\Dns\Config\Config; | |
6 | ||
7 | class ConfigTest extends TestCase | |
8 | { | |
9 | public function testLoadsSystemDefault() | |
10 | { | |
11 | $config = Config::loadSystemConfigBlocking(); | |
12 | ||
13 | $this->assertInstanceOf('React\Dns\Config\Config', $config); | |
14 | } | |
15 | ||
16 | public function testLoadsDefaultPath() | |
17 | { | |
18 | if (DIRECTORY_SEPARATOR === '\\') { | |
19 | $this->markTestSkipped('Not supported on Windows'); | |
20 | } | |
21 | ||
22 | $config = Config::loadResolvConfBlocking(); | |
23 | ||
24 | $this->assertInstanceOf('React\Dns\Config\Config', $config); | |
25 | } | |
26 | ||
27 | public function testLoadsFromExplicitPath() | |
28 | { | |
29 | $config = Config::loadResolvConfBlocking(__DIR__ . '/../Fixtures/etc/resolv.conf'); | |
30 | ||
31 | $this->assertEquals(array('8.8.8.8'), $config->nameservers); | |
32 | } | |
33 | ||
34 | /** | |
35 | * @expectedException RuntimeException | |
36 | */ | |
37 | public function testLoadThrowsWhenPathIsInvalid() | |
38 | { | |
39 | Config::loadResolvConfBlocking(__DIR__ . '/invalid.conf'); | |
40 | } | |
41 | ||
42 | public function testParsesSingleEntryFile() | |
43 | { | |
44 | $contents = 'nameserver 8.8.8.8'; | |
45 | $expected = array('8.8.8.8'); | |
46 | ||
47 | $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); | |
48 | $this->assertEquals($expected, $config->nameservers); | |
49 | } | |
50 | ||
51 | public function testParsesNameserverEntriesFromAverageFileCorrectly() | |
52 | { | |
53 | $contents = '# | |
54 | # Mac OS X Notice | |
55 | # | |
56 | # This file is not used by the host name and address resolution | |
57 | # or the DNS query routing mechanisms used by most processes on | |
58 | # this Mac OS X system. | |
59 | # | |
60 | # This file is automatically generated. | |
61 | # | |
62 | domain v.cablecom.net | |
63 | nameserver 127.0.0.1 | |
64 | nameserver ::1 | |
65 | '; | |
66 | $expected = array('127.0.0.1', '::1'); | |
67 | ||
68 | $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); | |
69 | $this->assertEquals($expected, $config->nameservers); | |
70 | } | |
71 | ||
72 | public function testParsesEmptyFileWithoutNameserverEntries() | |
73 | { | |
74 | $contents = ''; | |
75 | $expected = array(); | |
76 | ||
77 | $config = Config::loadResolvConfBlocking('data://text/plain;base64,'); | |
78 | $this->assertEquals($expected, $config->nameservers); | |
79 | } | |
80 | ||
81 | public function testParsesFileAndIgnoresCommentsAndInvalidNameserverEntries() | |
82 | { | |
83 | $contents = ' | |
84 | # nameserver 1.2.3.4 | |
85 | ; nameserver 2.3.4.5 | |
86 | ||
87 | nameserver 3.4.5.6 # nope | |
88 | nameserver 4.5.6.7 5.6.7.8 | |
89 | nameserver 6.7.8.9 | |
90 | NameServer 7.8.9.10 | |
91 | '; | |
92 | $expected = array(); | |
93 | ||
94 | $config = Config::loadResolvConfBlocking('data://text/plain;base64,' . base64_encode($contents)); | |
95 | $this->assertEquals($expected, $config->nameservers); | |
96 | } | |
97 | ||
98 | public function testLoadsFromWmicOnWindows() | |
99 | { | |
100 | if (DIRECTORY_SEPARATOR !== '\\') { | |
101 | $this->markTestSkipped('Only on Windows'); | |
102 | } | |
103 | ||
104 | $config = Config::loadWmicBlocking(); | |
105 | ||
106 | $this->assertInstanceOf('React\Dns\Config\Config', $config); | |
107 | } | |
108 | ||
109 | public function testLoadsSingleEntryFromWmicOutput() | |
110 | { | |
111 | $contents = ' | |
112 | Node,DNSServerSearchOrder | |
113 | ACE, | |
114 | ACE,{192.168.2.1} | |
115 | ACE, | |
116 | '; | |
117 | $expected = array('192.168.2.1'); | |
118 | ||
119 | $config = Config::loadWmicBlocking($this->echoCommand($contents)); | |
120 | ||
121 | $this->assertEquals($expected, $config->nameservers); | |
122 | } | |
123 | ||
124 | public function testLoadsEmptyListFromWmicOutput() | |
125 | { | |
126 | $contents = ' | |
127 | Node,DNSServerSearchOrder | |
128 | ACE, | |
129 | '; | |
130 | $expected = array(); | |
131 | ||
132 | $config = Config::loadWmicBlocking($this->echoCommand($contents)); | |
133 | ||
134 | $this->assertEquals($expected, $config->nameservers); | |
135 | } | |
136 | ||
137 | public function testLoadsSingleEntryForMultipleNicsFromWmicOutput() | |
138 | { | |
139 | $contents = ' | |
140 | Node,DNSServerSearchOrder | |
141 | ACE, | |
142 | ACE,{192.168.2.1} | |
143 | ACE, | |
144 | ACE,{192.168.2.2} | |
145 | ACE, | |
146 | '; | |
147 | $expected = array('192.168.2.1', '192.168.2.2'); | |
148 | ||
149 | $config = Config::loadWmicBlocking($this->echoCommand($contents)); | |
150 | ||
151 | $this->assertEquals($expected, $config->nameservers); | |
152 | } | |
153 | ||
154 | public function testLoadsMultipleEntriesForSingleNicWithSemicolonFromWmicOutput() | |
155 | { | |
156 | $contents = ' | |
157 | Node,DNSServerSearchOrder | |
158 | ACE, | |
159 | ACE,{192.168.2.1;192.168.2.2} | |
160 | ACE, | |
161 | '; | |
162 | $expected = array('192.168.2.1', '192.168.2.2'); | |
163 | ||
164 | $config = Config::loadWmicBlocking($this->echoCommand($contents)); | |
165 | ||
166 | $this->assertEquals($expected, $config->nameservers); | |
167 | } | |
168 | ||
169 | public function testLoadsMultipleEntriesForSingleNicWithQuotesFromWmicOutput() | |
170 | { | |
171 | $contents = ' | |
172 | Node,DNSServerSearchOrder | |
173 | ACE, | |
174 | ACE,{"192.168.2.1","192.168.2.2"} | |
175 | ACE, | |
176 | '; | |
177 | $expected = array('192.168.2.1', '192.168.2.2'); | |
178 | ||
179 | $config = Config::loadWmicBlocking($this->echoCommand($contents)); | |
180 | ||
181 | $this->assertEquals($expected, $config->nameservers); | |
182 | } | |
183 | ||
184 | private function echoCommand($output) | |
185 | { | |
186 | return 'echo ' . escapeshellarg($output); | |
187 | } | |
188 | } |
0 | <?php | |
1 | ||
2 | namespace React\Test\Dns\Config; | |
3 | ||
4 | use PHPUnit\Framework\TestCase; | |
5 | use React\Dns\Config\FilesystemFactory; | |
6 | ||
7 | class FilesystemFactoryTest extends TestCase | |
8 | { | |
9 | /** @test */ | |
10 | public function parseEtcResolvConfShouldParseCorrectly() | |
11 | { | |
12 | $contents = '# | |
13 | # Mac OS X Notice | |
14 | # | |
15 | # This file is not used by the host name and address resolution | |
16 | # or the DNS query routing mechanisms used by most processes on | |
17 | # this Mac OS X system. | |
18 | # | |
19 | # This file is automatically generated. | |
20 | # | |
21 | domain v.cablecom.net | |
22 | nameserver 127.0.0.1 | |
23 | nameserver 8.8.8.8 | |
24 | '; | |
25 | $expected = array('127.0.0.1', '8.8.8.8'); | |
26 | ||
27 | $capturedConfig = null; | |
28 | ||
29 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
30 | $factory = new FilesystemFactory($loop); | |
31 | $factory->parseEtcResolvConf($contents)->then(function ($config) use (&$capturedConfig) { | |
32 | $capturedConfig = $config; | |
33 | }); | |
34 | ||
35 | $this->assertNotNull($capturedConfig); | |
36 | $this->assertSame($expected, $capturedConfig->nameservers); | |
37 | } | |
38 | ||
39 | /** @test */ | |
40 | public function createShouldLoadStuffFromFilesystem() | |
41 | { | |
42 | $this->markTestIncomplete('Filesystem API is incomplete'); | |
43 | ||
44 | $expected = array('8.8.8.8'); | |
45 | ||
46 | $triggerListener = null; | |
47 | $capturedConfig = null; | |
48 | ||
49 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
50 | $loop | |
51 | ->expects($this->once()) | |
52 | ->method('addReadStream') | |
53 | ->will($this->returnCallback(function ($stream, $listener) use (&$triggerListener) { | |
54 | $triggerListener = function () use ($stream, $listener) { | |
55 | call_user_func($listener, $stream); | |
56 | }; | |
57 | })); | |
58 | ||
59 | $factory = new FilesystemFactory($loop); | |
60 | $factory->create(__DIR__.'/../Fixtures/etc/resolv.conf')->then(function ($config) use (&$capturedConfig) { | |
61 | $capturedConfig = $config; | |
62 | }); | |
63 | ||
64 | $triggerListener(); | |
65 | ||
66 | $this->assertNotNull($capturedConfig); | |
67 | $this->assertSame($expected, $capturedConfig->nameservers); | |
68 | } | |
69 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Config; | |
3 | ||
4 | use React\Tests\Dns\TestCase; | |
5 | use React\Dns\Config\HostsFile; | |
6 | ||
7 | class HostsFileTest extends TestCase | |
8 | { | |
9 | public function testLoadsFromDefaultPath() | |
10 | { | |
11 | $hosts = HostsFile::loadFromPathBlocking(); | |
12 | ||
13 | $this->assertInstanceOf('React\Dns\Config\HostsFile', $hosts); | |
14 | } | |
15 | ||
16 | public function testDefaultShouldHaveLocalhostMapped() | |
17 | { | |
18 | if (DIRECTORY_SEPARATOR === '\\') { | |
19 | $this->markTestSkipped('Not supported on Windows'); | |
20 | } | |
21 | ||
22 | $hosts = HostsFile::loadFromPathBlocking(); | |
23 | ||
24 | $this->assertContains('127.0.0.1', $hosts->getIpsForHost('localhost')); | |
25 | } | |
26 | ||
27 | /** | |
28 | * @expectedException RuntimeException | |
29 | */ | |
30 | public function testLoadThrowsForInvalidPath() | |
31 | { | |
32 | HostsFile::loadFromPathBlocking('does/not/exist'); | |
33 | } | |
34 | ||
35 | public function testContainsSingleLocalhostEntry() | |
36 | { | |
37 | $hosts = new HostsFile('127.0.0.1 localhost'); | |
38 | ||
39 | $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost')); | |
40 | $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); | |
41 | } | |
42 | ||
43 | public function testNonIpReturnsNothingForInvalidHosts() | |
44 | { | |
45 | $hosts = new HostsFile('a b'); | |
46 | ||
47 | $this->assertEquals(array(), $hosts->getIpsForHost('a')); | |
48 | $this->assertEquals(array(), $hosts->getIpsForHost('b')); | |
49 | } | |
50 | ||
51 | public function testIgnoresIpv6ZoneId() | |
52 | { | |
53 | $hosts = new HostsFile('fe80::1%lo0 localhost'); | |
54 | ||
55 | $this->assertEquals(array('fe80::1'), $hosts->getIpsForHost('localhost')); | |
56 | } | |
57 | ||
58 | public function testSkipsComments() | |
59 | { | |
60 | $hosts = new HostsFile('# start' . PHP_EOL .'#127.0.0.1 localhost' . PHP_EOL . '127.0.0.2 localhost # example.com'); | |
61 | ||
62 | $this->assertEquals(array('127.0.0.2'), $hosts->getIpsForHost('localhost')); | |
63 | $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); | |
64 | } | |
65 | ||
66 | public function testContainsSingleLocalhostEntryWithCaseIgnored() | |
67 | { | |
68 | $hosts = new HostsFile('127.0.0.1 LocalHost'); | |
69 | ||
70 | $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('LOCALHOST')); | |
71 | } | |
72 | ||
73 | public function testEmptyFileContainsNothing() | |
74 | { | |
75 | $hosts = new HostsFile(''); | |
76 | ||
77 | $this->assertEquals(array(), $hosts->getIpsForHost('example.com')); | |
78 | } | |
79 | ||
80 | public function testSingleEntryWithMultipleNames() | |
81 | { | |
82 | $hosts = new HostsFile('127.0.0.1 localhost example.com'); | |
83 | ||
84 | $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('example.com')); | |
85 | $this->assertEquals(array('127.0.0.1'), $hosts->getIpsForHost('localhost')); | |
86 | } | |
87 | ||
88 | public function testMergesEntriesOverMultipleLines() | |
89 | { | |
90 | $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"); | |
91 | ||
92 | $this->assertEquals(array('127.0.0.1', '127.0.0.2', '127.0.0.3', '127.0.0.4'), $hosts->getIpsForHost('localhost')); | |
93 | } | |
94 | ||
95 | public function testMergesIpv4AndIpv6EntriesOverMultipleLines() | |
96 | { | |
97 | $hosts = new HostsFile("127.0.0.1 localhost\n::1 localhost"); | |
98 | ||
99 | $this->assertEquals(array('127.0.0.1', '::1'), $hosts->getIpsForHost('localhost')); | |
100 | } | |
101 | ||
102 | public function testReverseLookup() | |
103 | { | |
104 | $hosts = new HostsFile('127.0.0.1 localhost'); | |
105 | ||
106 | $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1')); | |
107 | $this->assertEquals(array(), $hosts->getHostsForIp('192.168.1.1')); | |
108 | } | |
109 | ||
110 | public function testReverseSkipsComments() | |
111 | { | |
112 | $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"); | |
113 | ||
114 | $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1')); | |
115 | $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.2')); | |
116 | $this->assertEquals(array('example.org'), $hosts->getHostsForIp('127.0.0.3')); | |
117 | } | |
118 | ||
119 | public function testReverseNonIpReturnsNothing() | |
120 | { | |
121 | $hosts = new HostsFile('127.0.0.1 localhost'); | |
122 | ||
123 | $this->assertEquals(array(), $hosts->getHostsForIp('localhost')); | |
124 | $this->assertEquals(array(), $hosts->getHostsForIp('127.0.0.1.1')); | |
125 | } | |
126 | ||
127 | public function testReverseNonIpReturnsNothingForInvalidHosts() | |
128 | { | |
129 | $hosts = new HostsFile('a b'); | |
130 | ||
131 | $this->assertEquals(array(), $hosts->getHostsForIp('a')); | |
132 | $this->assertEquals(array(), $hosts->getHostsForIp('b')); | |
133 | } | |
134 | ||
135 | public function testReverseLookupReturnsLowerCaseHost() | |
136 | { | |
137 | $hosts = new HostsFile('127.0.0.1 LocalHost'); | |
138 | ||
139 | $this->assertEquals(array('localhost'), $hosts->getHostsForIp('127.0.0.1')); | |
140 | } | |
141 | ||
142 | public function testReverseLookupChecksNormalizedIpv6() | |
143 | { | |
144 | $hosts = new HostsFile('FE80::00a1 localhost'); | |
145 | ||
146 | $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::A1')); | |
147 | } | |
148 | ||
149 | public function testReverseLookupIgnoresIpv6ZoneId() | |
150 | { | |
151 | $hosts = new HostsFile('fe80::1%lo0 localhost'); | |
152 | ||
153 | $this->assertEquals(array('localhost'), $hosts->getHostsForIp('fe80::1')); | |
154 | } | |
155 | ||
156 | public function testReverseLookupReturnsMultipleHostsOverSingleLine() | |
157 | { | |
158 | $hosts = new HostsFile("::1 ip6-localhost ip6-loopback"); | |
159 | ||
160 | $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1')); | |
161 | } | |
162 | ||
163 | public function testReverseLookupReturnsMultipleHostsOverMultipleLines() | |
164 | { | |
165 | $hosts = new HostsFile("::1 ip6-localhost\n::1 ip6-loopback"); | |
166 | ||
167 | $this->assertEquals(array('ip6-localhost', 'ip6-loopback'), $hosts->getHostsForIp('::1')); | |
168 | } | |
169 | } |
0 | nameserver 8.8.8.8 |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns; | |
3 | ||
4 | use React\Tests\Dns\TestCase; | |
5 | use React\EventLoop\Factory as LoopFactory; | |
6 | use React\Dns\Resolver\Resolver; | |
7 | use React\Dns\Resolver\Factory; | |
8 | ||
9 | class FunctionalTest extends TestCase | |
10 | { | |
11 | public function setUp() | |
12 | { | |
13 | $this->loop = LoopFactory::create(); | |
14 | ||
15 | $factory = new Factory(); | |
16 | $this->resolver = $factory->create('8.8.8.8', $this->loop); | |
17 | } | |
18 | ||
19 | public function testResolveLocalhostResolves() | |
20 | { | |
21 | $promise = $this->resolver->resolve('localhost'); | |
22 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); | |
23 | ||
24 | $this->loop->run(); | |
25 | } | |
26 | ||
27 | /** | |
28 | * @group internet | |
29 | */ | |
30 | public function testResolveGoogleResolves() | |
31 | { | |
32 | $promise = $this->resolver->resolve('google.com'); | |
33 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); | |
34 | ||
35 | $this->loop->run(); | |
36 | } | |
37 | ||
38 | /** | |
39 | * @group internet | |
40 | */ | |
41 | public function testResolveInvalidRejects() | |
42 | { | |
43 | $promise = $this->resolver->resolve('example.invalid'); | |
44 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); | |
45 | ||
46 | $this->loop->run(); | |
47 | } | |
48 | ||
49 | public function testResolveCancelledRejectsImmediately() | |
50 | { | |
51 | $promise = $this->resolver->resolve('google.com'); | |
52 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); | |
53 | $promise->cancel(); | |
54 | ||
55 | $time = microtime(true); | |
56 | $this->loop->run(); | |
57 | $time = microtime(true) - $time; | |
58 | ||
59 | $this->assertLessThan(0.1, $time); | |
60 | } | |
61 | ||
62 | public function testInvalidResolverDoesNotResolveGoogle() | |
63 | { | |
64 | $factory = new Factory(); | |
65 | $this->resolver = $factory->create('255.255.255.255', $this->loop); | |
66 | ||
67 | $promise = $this->resolver->resolve('google.com'); | |
68 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); | |
69 | } | |
70 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Model; | |
3 | ||
4 | use PHPUnit\Framework\TestCase; | |
5 | use React\Dns\Query\Query; | |
6 | use React\Dns\Model\Message; | |
7 | ||
8 | class MessageTest extends TestCase | |
9 | { | |
10 | public function testCreateRequestDesiresRecusion() | |
11 | { | |
12 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
13 | $request = Message::createRequestForQuery($query); | |
14 | ||
15 | $this->assertTrue($request->header->isQuery()); | |
16 | $this->assertSame(1, $request->header->get('rd')); | |
17 | } | |
18 | ||
19 | public function testCreateResponseWithNoAnswers() | |
20 | { | |
21 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
22 | $answers = array(); | |
23 | $request = Message::createResponseWithAnswersForQuery($query, $answers); | |
24 | ||
25 | $this->assertFalse($request->header->isQuery()); | |
26 | $this->assertTrue($request->header->isResponse()); | |
27 | $this->assertEquals(0, $request->header->get('anCount')); | |
28 | } | |
29 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Protocol; | |
3 | ||
4 | use PHPUnit\Framework\TestCase; | |
5 | use React\Dns\Protocol\BinaryDumper; | |
6 | use React\Dns\Model\Message; | |
7 | ||
8 | class BinaryDumperTest extends TestCase | |
9 | { | |
10 | public function testRequestToBinary() | |
11 | { | |
12 | $data = ""; | |
13 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
14 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
15 | $data .= "00 01 00 01"; // question: type A, class IN | |
16 | ||
17 | $expected = $this->formatHexDump(str_replace(' ', '', $data), 2); | |
18 | ||
19 | $request = new Message(); | |
20 | $request->header->set('id', 0x7262); | |
21 | $request->header->set('rd', 1); | |
22 | ||
23 | $request->questions[] = array( | |
24 | 'name' => 'igor.io', | |
25 | 'type' => Message::TYPE_A, | |
26 | 'class' => Message::CLASS_IN, | |
27 | ); | |
28 | ||
29 | $request->prepare(); | |
30 | ||
31 | $dumper = new BinaryDumper(); | |
32 | $data = $dumper->toBinary($request); | |
33 | $data = $this->convertBinaryToHexDump($data); | |
34 | ||
35 | $this->assertSame($expected, $data); | |
36 | } | |
37 | ||
38 | private function convertBinaryToHexDump($input) | |
39 | { | |
40 | return $this->formatHexDump(implode('', unpack('H*', $input))); | |
41 | } | |
42 | ||
43 | private function formatHexDump($input) | |
44 | { | |
45 | return implode(' ', str_split($input, 2)); | |
46 | } | |
47 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Protocol; | |
3 | ||
4 | use PHPUnit\Framework\TestCase; | |
5 | use React\Dns\Protocol\Parser; | |
6 | use React\Dns\Model\Message; | |
7 | ||
8 | class ParserTest extends TestCase | |
9 | { | |
10 | public function setUp() | |
11 | { | |
12 | $this->parser = new Parser(); | |
13 | } | |
14 | ||
15 | /** | |
16 | * @dataProvider provideConvertTcpDumpToBinary | |
17 | */ | |
18 | public function testConvertTcpDumpToBinary($expected, $data) | |
19 | { | |
20 | $this->assertSame($expected, $this->convertTcpDumpToBinary($data)); | |
21 | } | |
22 | ||
23 | public function provideConvertTcpDumpToBinary() | |
24 | { | |
25 | return array( | |
26 | array(chr(0x72).chr(0x62), "72 62"), | |
27 | array(chr(0x72).chr(0x62).chr(0x01).chr(0x00), "72 62 01 00"), | |
28 | array(chr(0x72).chr(0x62).chr(0x01).chr(0x00).chr(0x00).chr(0x01), "72 62 01 00 00 01"), | |
29 | array(chr(0x01).chr(0x00).chr(0x01), "01 00 01"), | |
30 | ); | |
31 | } | |
32 | ||
33 | public function testParseRequest() | |
34 | { | |
35 | $data = ""; | |
36 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
37 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
38 | $data .= "00 01 00 01"; // question: type A, class IN | |
39 | ||
40 | $data = $this->convertTcpDumpToBinary($data); | |
41 | ||
42 | $request = $this->parser->parseMessage($data); | |
43 | ||
44 | $header = $request->header; | |
45 | $this->assertSame(0x7262, $header->get('id')); | |
46 | $this->assertSame(1, $header->get('qdCount')); | |
47 | $this->assertSame(0, $header->get('anCount')); | |
48 | $this->assertSame(0, $header->get('nsCount')); | |
49 | $this->assertSame(0, $header->get('arCount')); | |
50 | $this->assertSame(0, $header->get('qr')); | |
51 | $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); | |
52 | $this->assertSame(0, $header->get('aa')); | |
53 | $this->assertSame(0, $header->get('tc')); | |
54 | $this->assertSame(1, $header->get('rd')); | |
55 | $this->assertSame(0, $header->get('ra')); | |
56 | $this->assertSame(0, $header->get('z')); | |
57 | $this->assertSame(Message::RCODE_OK, $header->get('rcode')); | |
58 | ||
59 | $this->assertCount(1, $request->questions); | |
60 | $this->assertSame('igor.io', $request->questions[0]['name']); | |
61 | $this->assertSame(Message::TYPE_A, $request->questions[0]['type']); | |
62 | $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']); | |
63 | } | |
64 | ||
65 | public function testParseResponse() | |
66 | { | |
67 | $data = ""; | |
68 | $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header | |
69 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
70 | $data .= "00 01 00 01"; // question: type A, class IN | |
71 | $data .= "c0 0c"; // answer: offset pointer to igor.io | |
72 | $data .= "00 01 00 01"; // answer: type A, class IN | |
73 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
74 | $data .= "00 04"; // answer: rdlength 4 | |
75 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 | |
76 | ||
77 | $data = $this->convertTcpDumpToBinary($data); | |
78 | ||
79 | $response = $this->parser->parseMessage($data); | |
80 | ||
81 | $header = $response->header; | |
82 | $this->assertSame(0x7262, $header->get('id')); | |
83 | $this->assertSame(1, $header->get('qdCount')); | |
84 | $this->assertSame(1, $header->get('anCount')); | |
85 | $this->assertSame(0, $header->get('nsCount')); | |
86 | $this->assertSame(0, $header->get('arCount')); | |
87 | $this->assertSame(1, $header->get('qr')); | |
88 | $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); | |
89 | $this->assertSame(0, $header->get('aa')); | |
90 | $this->assertSame(0, $header->get('tc')); | |
91 | $this->assertSame(1, $header->get('rd')); | |
92 | $this->assertSame(1, $header->get('ra')); | |
93 | $this->assertSame(0, $header->get('z')); | |
94 | $this->assertSame(Message::RCODE_OK, $header->get('rcode')); | |
95 | ||
96 | $this->assertCount(1, $response->questions); | |
97 | $this->assertSame('igor.io', $response->questions[0]['name']); | |
98 | $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); | |
99 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
100 | ||
101 | $this->assertCount(1, $response->answers); | |
102 | $this->assertSame('igor.io', $response->answers[0]->name); | |
103 | $this->assertSame(Message::TYPE_A, $response->answers[0]->type); | |
104 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
105 | $this->assertSame(86400, $response->answers[0]->ttl); | |
106 | $this->assertSame('178.79.169.131', $response->answers[0]->data); | |
107 | } | |
108 | ||
109 | public function testParseQuestionWithTwoQuestions() | |
110 | { | |
111 | $data = ""; | |
112 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
113 | $data .= "00 01 00 01"; // question: type A, class IN | |
114 | $data .= "03 77 77 77 04 69 67 6f 72 02 69 6f 00"; // question: www.igor.io | |
115 | $data .= "00 01 00 01"; // question: type A, class IN | |
116 | ||
117 | $data = $this->convertTcpDumpToBinary($data); | |
118 | ||
119 | $request = new Message(); | |
120 | $request->header->set('qdCount', 2); | |
121 | $request->data = $data; | |
122 | ||
123 | $this->parser->parseQuestion($request); | |
124 | ||
125 | $this->assertCount(2, $request->questions); | |
126 | $this->assertSame('igor.io', $request->questions[0]['name']); | |
127 | $this->assertSame(Message::TYPE_A, $request->questions[0]['type']); | |
128 | $this->assertSame(Message::CLASS_IN, $request->questions[0]['class']); | |
129 | $this->assertSame('www.igor.io', $request->questions[1]['name']); | |
130 | $this->assertSame(Message::TYPE_A, $request->questions[1]['type']); | |
131 | $this->assertSame(Message::CLASS_IN, $request->questions[1]['class']); | |
132 | } | |
133 | ||
134 | public function testParseAnswerWithInlineData() | |
135 | { | |
136 | $data = ""; | |
137 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
138 | $data .= "00 01 00 01"; // answer: type A, class IN | |
139 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
140 | $data .= "00 04"; // answer: rdlength 4 | |
141 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 | |
142 | ||
143 | $data = $this->convertTcpDumpToBinary($data); | |
144 | ||
145 | $response = new Message(); | |
146 | $response->header->set('anCount', 1); | |
147 | $response->data = $data; | |
148 | ||
149 | $this->parser->parseAnswer($response); | |
150 | ||
151 | $this->assertCount(1, $response->answers); | |
152 | $this->assertSame('igor.io', $response->answers[0]->name); | |
153 | $this->assertSame(Message::TYPE_A, $response->answers[0]->type); | |
154 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
155 | $this->assertSame(86400, $response->answers[0]->ttl); | |
156 | $this->assertSame('178.79.169.131', $response->answers[0]->data); | |
157 | } | |
158 | ||
159 | public function testParseResponseWithCnameAndOffsetPointers() | |
160 | { | |
161 | $data = ""; | |
162 | $data .= "9e 8d 81 80 00 01 00 01 00 00 00 00"; // header | |
163 | $data .= "04 6d 61 69 6c 06 67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: mail.google.com | |
164 | $data .= "00 05 00 01"; // question: type CNAME, class IN | |
165 | $data .= "c0 0c"; // answer: offset pointer to mail.google.com | |
166 | $data .= "00 05 00 01"; // answer: type CNAME, class IN | |
167 | $data .= "00 00 a8 9c"; // answer: ttl 43164 | |
168 | $data .= "00 0f"; // answer: rdlength 15 | |
169 | $data .= "0a 67 6f 6f 67 6c 65 6d 61 69 6c 01 6c"; // answer: rdata googlemail.l. | |
170 | $data .= "c0 11"; // answer: rdata offset pointer to google.com | |
171 | ||
172 | $data = $this->convertTcpDumpToBinary($data); | |
173 | ||
174 | $response = $this->parser->parseMessage($data); | |
175 | ||
176 | $this->assertCount(1, $response->questions); | |
177 | $this->assertSame('mail.google.com', $response->questions[0]['name']); | |
178 | $this->assertSame(Message::TYPE_CNAME, $response->questions[0]['type']); | |
179 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
180 | ||
181 | $this->assertCount(1, $response->answers); | |
182 | $this->assertSame('mail.google.com', $response->answers[0]->name); | |
183 | $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); | |
184 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
185 | $this->assertSame(43164, $response->answers[0]->ttl); | |
186 | $this->assertSame('googlemail.l.google.com', $response->answers[0]->data); | |
187 | } | |
188 | ||
189 | public function testParseAAAAResponse() | |
190 | { | |
191 | $data = ""; | |
192 | $data .= "cd 72 81 80 00 01 00 01 00 00 00 00 06"; // header | |
193 | $data .= "67 6f 6f 67 6c 65 03 63 6f 6d 00"; // question: google.com | |
194 | $data .= "00 1c 00 01"; // question: type AAAA, class IN | |
195 | $data .= "c0 0c"; // answer: offset pointer to google.com | |
196 | $data .= "00 1c 00 01"; // answer: type AAAA, class IN | |
197 | $data .= "00 00 01 2b"; // answer: ttl 299 | |
198 | $data .= "00 10"; // answer: rdlength 16 | |
199 | $data .= "2a 00 14 50 40 09 08 09 00 00 00 00 00 00 20 0e"; // answer: 2a00:1450:4009:809::200e | |
200 | ||
201 | $data = $this->convertTcpDumpToBinary($data); | |
202 | ||
203 | $response = $this->parser->parseMessage($data); | |
204 | ||
205 | $header = $response->header; | |
206 | $this->assertSame(0xcd72, $header->get('id')); | |
207 | $this->assertSame(1, $header->get('qdCount')); | |
208 | $this->assertSame(1, $header->get('anCount')); | |
209 | $this->assertSame(0, $header->get('nsCount')); | |
210 | $this->assertSame(0, $header->get('arCount')); | |
211 | $this->assertSame(1, $header->get('qr')); | |
212 | $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); | |
213 | $this->assertSame(0, $header->get('aa')); | |
214 | $this->assertSame(0, $header->get('tc')); | |
215 | $this->assertSame(1, $header->get('rd')); | |
216 | $this->assertSame(1, $header->get('ra')); | |
217 | $this->assertSame(0, $header->get('z')); | |
218 | $this->assertSame(Message::RCODE_OK, $header->get('rcode')); | |
219 | ||
220 | $this->assertCount(1, $response->questions); | |
221 | $this->assertSame('google.com', $response->questions[0]['name']); | |
222 | $this->assertSame(Message::TYPE_AAAA, $response->questions[0]['type']); | |
223 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
224 | ||
225 | $this->assertCount(1, $response->answers); | |
226 | $this->assertSame('google.com', $response->answers[0]->name); | |
227 | $this->assertSame(Message::TYPE_AAAA, $response->answers[0]->type); | |
228 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
229 | $this->assertSame(299, $response->answers[0]->ttl); | |
230 | $this->assertSame('2a00:1450:4009:809::200e', $response->answers[0]->data); | |
231 | } | |
232 | ||
233 | public function testParseResponseWithTwoAnswers() | |
234 | { | |
235 | $data = ""; | |
236 | $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header | |
237 | $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; | |
238 | // question: io.whois-servers.net | |
239 | $data .= "00 01 00 01"; // question: type A, class IN | |
240 | $data .= "c0 0c"; // answer: offset pointer to io.whois-servers.net | |
241 | $data .= "00 05 00 01"; // answer: type CNAME, class IN | |
242 | $data .= "00 00 00 29"; // answer: ttl 41 | |
243 | $data .= "00 0e"; // answer: rdlength 14 | |
244 | $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // answer: rdata whois.nic.io | |
245 | $data .= "c0 32"; // answer: offset pointer to whois.nic.io | |
246 | $data .= "00 01 00 01"; // answer: type CNAME, class IN | |
247 | $data .= "00 00 0d f7"; // answer: ttl 3575 | |
248 | $data .= "00 04"; // answer: rdlength 4 | |
249 | $data .= "c1 df 4e 98"; // answer: rdata 193.223.78.152 | |
250 | ||
251 | $data = $this->convertTcpDumpToBinary($data); | |
252 | ||
253 | $response = $this->parser->parseMessage($data); | |
254 | ||
255 | $this->assertCount(1, $response->questions); | |
256 | $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); | |
257 | $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); | |
258 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
259 | ||
260 | $this->assertCount(2, $response->answers); | |
261 | ||
262 | $this->assertSame('io.whois-servers.net', $response->answers[0]->name); | |
263 | $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); | |