Update upstream source from tag 'upstream/1.2.0'
Update to upstream version '1.2.0'
with Debian dir a37969798fad6746eac1d78985c18fec17a7bc73
Dominik George
4 years ago
7 | 7 | - 7.0 |
8 | 8 | - 7.1 |
9 | 9 | - 7.2 |
10 | - hhvm # ignore errors, see below | |
10 | - 7.3 | |
11 | # - hhvm # requires legacy phpunit & ignore errors, see below | |
11 | 12 | |
12 | 13 | # lock distro so new future defaults will not break the build |
13 | 14 | dist: trusty |
16 | 17 | include: |
17 | 18 | - php: 5.3 |
18 | 19 | dist: precise |
20 | - php: hhvm | |
21 | install: composer require phpunit/phpunit:^5 --dev --no-interaction | |
19 | 22 | allow_failures: |
20 | 23 | - php: hhvm |
21 | 24 |
0 | 0 | # Changelog |
1 | ||
2 | ## 1.2.0 (2019-08-15) | |
3 | ||
4 | * Feature: Add `TcpTransportExecutor` to send DNS queries over TCP/IP connection, | |
5 | add `SelectiveTransportExecutor` to retry with TCP if UDP is truncated and | |
6 | automatically select transport protocol when no explicit `udp://` or `tcp://` scheme is given in `Factory`. | |
7 | (#145, #146, #147 and #148 by @clue) | |
8 | ||
9 | * Feature: Support escaping literal dots and special characters in domain names. | |
10 | (#144 by @clue) | |
11 | ||
12 | ## 1.1.0 (2019-07-18) | |
13 | ||
14 | * Feature: Support parsing `CAA` and `SSHFP` records. | |
15 | (#141 and #142 by @clue) | |
16 | ||
17 | * Feature: Add `ResolverInterface` as common interface for `Resolver` class. | |
18 | (#139 by @clue) | |
19 | ||
20 | * Fix: Add missing private property definitions and | |
21 | remove unneeded dependency on `react/stream`. | |
22 | (#140 and #143 by @clue) | |
23 | ||
24 | ## 1.0.0 (2019-07-11) | |
25 | ||
26 | * First stable LTS release, now following [SemVer](https://semver.org/). | |
27 | We'd like to emphasize that this component is production ready and battle-tested. | |
28 | We plan to support all long-term support (LTS) releases for at least 24 months, | |
29 | so you have a rock-solid foundation to build on top of. | |
30 | ||
31 | This update involves a number of BC breaks due to dropped support for | |
32 | deprecated functionality and some internal API cleanup. We've tried hard to | |
33 | avoid BC breaks where possible and minimize impact otherwise. We expect that | |
34 | most consumers of this package will actually not be affected by any BC | |
35 | breaks, see below for more details: | |
36 | ||
37 | * BC break: Delete all deprecated APIs, use `Query` objects for `Message` questions | |
38 | instead of nested arrays and increase code coverage to 100%. | |
39 | (#130 by @clue) | |
40 | ||
41 | * BC break: Move `$nameserver` from `ExecutorInterface` to `UdpTransportExecutor`, | |
42 | remove advanced/internal `UdpTransportExecutor` args for `Parser`/`BinaryDumper` and | |
43 | add API documentation for `ExecutorInterface`. | |
44 | (#135, #137 and #138 by @clue) | |
45 | ||
46 | * BC break: Replace `HeaderBag` attributes with simple `Message` properties. | |
47 | (#132 by @clue) | |
48 | ||
49 | * BC break: Mark all `Record` attributes as required, add documentation vs `Query`. | |
50 | (#136 by @clue) | |
51 | ||
52 | * BC break: Mark all classes as final to discourage inheritance | |
53 | (#134 by @WyriHaximus) | |
54 | ||
55 | ## 0.4.19 (2019-07-10) | |
56 | ||
57 | * Feature: Avoid garbage references when DNS resolution rejects on legacy PHP <= 5.6. | |
58 | (#133 by @clue) | |
59 | ||
60 | ## 0.4.18 (2019-09-07) | |
61 | ||
62 | * Feature / Fix: Implement `CachingExecutor` using cache TTL, deprecate old `CachedExecutor`, | |
63 | respect TTL from response records when caching and do not cache truncated responses. | |
64 | (#129 by @clue) | |
65 | ||
66 | * Feature: Limit cache size to 256 last responses by default. | |
67 | (#127 by @clue) | |
68 | ||
69 | * Feature: Cooperatively resolve hosts to avoid running same query concurrently. | |
70 | (#125 by @clue) | |
71 | ||
72 | ## 0.4.17 (2019-04-01) | |
73 | ||
74 | * Feature: Support parsing `authority` and `additional` records from DNS response. | |
75 | (#123 by @clue) | |
76 | ||
77 | * Feature: Support dumping records as part of outgoing binary DNS message. | |
78 | (#124 by @clue) | |
79 | ||
80 | * Feature: Forward compatibility with upcoming Cache v0.6 and Cache v1.0 | |
81 | (#121 by @clue) | |
82 | ||
83 | * Improve test suite to add forward compatibility with PHPUnit 7, | |
84 | test against PHP 7.3 and use legacy PHPUnit 5 on legacy HHVM. | |
85 | (#122 by @clue) | |
1 | 86 | |
2 | 87 | ## 0.4.16 (2018-11-11) |
3 | 88 |
12 | 12 | * [Basic usage](#basic-usage) |
13 | 13 | * [Caching](#caching) |
14 | 14 | * [Custom cache adapter](#custom-cache-adapter) |
15 | * [Resolver](#resolver) | |
15 | * [ResolverInterface](#resolverinterface) | |
16 | 16 | * [resolve()](#resolve) |
17 | 17 | * [resolveAll()](#resolveall) |
18 | 18 | * [Advanced usage](#advanced-usage) |
19 | 19 | * [UdpTransportExecutor](#udptransportexecutor) |
20 | * [TcpTransportExecutor](#tcptransportexecutor) | |
21 | * [SelectiveTransportExecutor](#selectivetransportexecutor) | |
20 | 22 | * [HostsFileExecutor](#hostsfileexecutor) |
21 | 23 | * [Install](#install) |
22 | 24 | * [Tests](#tests) |
110 | 112 | |
111 | 113 | See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations). |
112 | 114 | |
113 | ## Resolver | |
115 | ## ResolverInterface | |
116 | ||
117 | <a id="resolver"><!-- legacy reference --></a> | |
114 | 118 | |
115 | 119 | ### resolve() |
116 | 120 | |
207 | 211 | |
208 | 212 | ```php |
209 | 213 | $loop = Factory::create(); |
210 | $executor = new UdpTransportExecutor($loop); | |
214 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
211 | 215 | |
212 | 216 | $executor->query( |
213 | '8.8.8.8:53', | |
214 | 217 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) |
215 | 218 | )->then(function (Message $message) { |
216 | 219 | foreach ($message->answers as $answer) { |
228 | 231 | |
229 | 232 | ```php |
230 | 233 | $executor = new TimeoutExecutor( |
231 | new UdpTransportExecutor($loop), | |
234 | new UdpTransportExecutor($nameserver, $loop), | |
232 | 235 | 3.0, |
233 | 236 | $loop |
234 | 237 | ); |
241 | 244 | ```php |
242 | 245 | $executor = new RetryExecutor( |
243 | 246 | new TimeoutExecutor( |
244 | new UdpTransportExecutor($loop), | |
247 | new UdpTransportExecutor($nameserver, $loop), | |
245 | 248 | 3.0, |
246 | 249 | $loop |
250 | ) | |
251 | ); | |
252 | ``` | |
253 | ||
254 | Note that this executor is entirely async and as such allows you to execute | |
255 | any number of queries concurrently. You should probably limit the number of | |
256 | concurrent queries in your application or you're very likely going to face | |
257 | rate limitations and bans on the resolver end. For many common applications, | |
258 | you may want to avoid sending the same query multiple times when the first | |
259 | one is still pending, so you will likely want to use this in combination with | |
260 | a `CoopExecutor` like this: | |
261 | ||
262 | ```php | |
263 | $executor = new CoopExecutor( | |
264 | new RetryExecutor( | |
265 | new TimeoutExecutor( | |
266 | new UdpTransportExecutor($nameserver, $loop), | |
267 | 3.0, | |
268 | $loop | |
269 | ) | |
247 | 270 | ) |
248 | 271 | ); |
249 | 272 | ``` |
254 | 277 | packages. Higher-level components should take advantage of the Datagram |
255 | 278 | component instead of reimplementing this socket logic from scratch. |
256 | 279 | |
280 | ### TcpTransportExecutor | |
281 | ||
282 | The `TcpTransportExecutor` class can be used to | |
283 | send DNS queries over a TCP/IP stream transport. | |
284 | ||
285 | This is one of the main classes that send a DNS query to your DNS server. | |
286 | ||
287 | For more advanced usages one can utilize this class directly. | |
288 | The following example looks up the `IPv6` address for `reactphp.org`. | |
289 | ||
290 | ```php | |
291 | $loop = Factory::create(); | |
292 | $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
293 | ||
294 | $executor->query( | |
295 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
296 | )->then(function (Message $message) { | |
297 | foreach ($message->answers as $answer) { | |
298 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
299 | } | |
300 | }, 'printf'); | |
301 | ||
302 | $loop->run(); | |
303 | ``` | |
304 | ||
305 | See also [example #92](examples). | |
306 | ||
307 | Note that this executor does not implement a timeout, so you will very likely | |
308 | want to use this in combination with a `TimeoutExecutor` like this: | |
309 | ||
310 | ```php | |
311 | $executor = new TimeoutExecutor( | |
312 | new TcpTransportExecutor($nameserver, $loop), | |
313 | 3.0, | |
314 | $loop | |
315 | ); | |
316 | ``` | |
317 | ||
318 | Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP | |
319 | transport, so you do not necessarily have to implement any retry logic. | |
320 | ||
321 | Note that this executor is entirely async and as such allows you to execute | |
322 | queries concurrently. The first query will establish a TCP/IP socket | |
323 | connection to the DNS server which will be kept open for a short period. | |
324 | Additional queries will automatically reuse this existing socket connection | |
325 | to the DNS server, will pipeline multiple requests over this single | |
326 | connection and will keep an idle connection open for a short period. The | |
327 | initial TCP/IP connection overhead may incur a slight delay if you only send | |
328 | occasional queries – when sending a larger number of concurrent queries over | |
329 | an existing connection, it becomes increasingly more efficient and avoids | |
330 | creating many concurrent sockets like the UDP-based executor. You may still | |
331 | want to limit the number of (concurrent) queries in your application or you | |
332 | may be facing rate limitations and bans on the resolver end. For many common | |
333 | applications, you may want to avoid sending the same query multiple times | |
334 | when the first one is still pending, so you will likely want to use this in | |
335 | combination with a `CoopExecutor` like this: | |
336 | ||
337 | ```php | |
338 | $executor = new CoopExecutor( | |
339 | new TimeoutExecutor( | |
340 | new TcpTransportExecutor($nameserver, $loop), | |
341 | 3.0, | |
342 | $loop | |
343 | ) | |
344 | ); | |
345 | ``` | |
346 | ||
347 | > Internally, this class uses PHP's TCP/IP sockets and does not take advantage | |
348 | of [react/socket](https://github.com/reactphp/socket) purely for | |
349 | organizational reasons to avoid a cyclic dependency between the two | |
350 | packages. Higher-level components should take advantage of the Socket | |
351 | component instead of reimplementing this socket logic from scratch. | |
352 | ||
353 | ### SelectiveTransportExecutor | |
354 | ||
355 | The `SelectiveTransportExecutor` class can be used to | |
356 | Send DNS queries over a UDP or TCP/IP stream transport. | |
357 | ||
358 | This class will automatically choose the correct transport protocol to send | |
359 | a DNS query to your DNS server. It will always try to send it over the more | |
360 | efficient UDP transport first. If this query yields a size related issue | |
361 | (truncated messages), it will retry over a streaming TCP/IP transport. | |
362 | ||
363 | For more advanced usages one can utilize this class directly. | |
364 | The following example looks up the `IPv6` address for `reactphp.org`. | |
365 | ||
366 | ```php | |
367 | $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor); | |
368 | ||
369 | $executor->query( | |
370 | new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
371 | )->then(function (Message $message) { | |
372 | foreach ($message->answers as $answer) { | |
373 | echo 'IPv6: ' . $answer->data . PHP_EOL; | |
374 | } | |
375 | }, 'printf'); | |
376 | ``` | |
377 | ||
378 | Note that this executor only implements the logic to select the correct | |
379 | transport for the given DNS query. Implementing the correct transport logic, | |
380 | implementing timeouts and any retry logic is left up to the given executors, | |
381 | see also [`UdpTransportExecutor`](#udptransportexecutor) and | |
382 | [`TcpTransportExecutor`](#tcptransportexecutor) for more details. | |
383 | ||
384 | Note that this executor is entirely async and as such allows you to execute | |
385 | any number of queries concurrently. You should probably limit the number of | |
386 | concurrent queries in your application or you're very likely going to face | |
387 | rate limitations and bans on the resolver end. For many common applications, | |
388 | you may want to avoid sending the same query multiple times when the first | |
389 | one is still pending, so you will likely want to use this in combination with | |
390 | a `CoopExecutor` like this: | |
391 | ||
392 | ```php | |
393 | $executor = new CoopExecutor( | |
394 | new SelectiveTransportExecutor( | |
395 | $datagramExecutor, | |
396 | $streamExecutor | |
397 | ) | |
398 | ); | |
399 | ``` | |
400 | ||
257 | 401 | ### HostsFileExecutor |
258 | 402 | |
259 | 403 | Note that the above `UdpTransportExecutor` class always performs an actual DNS query. |
263 | 407 | ```php |
264 | 408 | $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking(); |
265 | 409 | |
266 | $executor = new UdpTransportExecutor($loop); | |
410 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
267 | 411 | $executor = new HostsFileExecutor($hosts, $executor); |
268 | 412 | |
269 | 413 | $executor->query( |
270 | '8.8.8.8:53', | |
271 | 414 | new Query('localhost', Message::TYPE_A, Message::CLASS_IN) |
272 | 415 | ); |
273 | 416 | ``` |
277 | 420 | The recommended way to install this library is [through Composer](https://getcomposer.org). |
278 | 421 | [New to Composer?](https://getcomposer.org/doc/00-intro.md) |
279 | 422 | |
423 | This project follows [SemVer](https://semver.org/). | |
280 | 424 | This will install the latest supported version: |
281 | 425 | |
282 | 426 | ```bash |
283 | $ composer require react/dns:^0.4.16 | |
427 | $ composer require react/dns:^1.2 | |
284 | 428 | ``` |
285 | 429 | |
286 | 430 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. |
4 | 4 | "license": "MIT", |
5 | 5 | "require": { |
6 | 6 | "php": ">=5.3.0", |
7 | "react/cache": "^0.5 || ^0.4 || ^0.3", | |
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" | |
7 | "react/cache": "^1.0 || ^0.6 || ^0.5", | |
8 | "react/event-loop": "^1.0 || ^0.5", | |
9 | "react/promise": "^2.7 || ^1.2.1", | |
10 | "react/promise-timer": "^1.2" | |
12 | 11 | }, |
13 | 12 | "require-dev": { |
14 | 13 | "clue/block-react": "^1.2", |
15 | "phpunit/phpunit": "^6.4 || ^5.7 || ^4.8.35" | |
14 | "phpunit/phpunit": "^7.0 || ^6.4 || ^5.7 || ^4.8.35" | |
16 | 15 | }, |
17 | 16 | "autoload": { |
18 | 17 | "psr-4": { "React\\Dns\\": "src" } |
0 | 0 | <?php |
1 | ||
2 | // $ php examples/12-all-types.php | |
3 | // $ php examples/12-all-types.php myserverplace.de SSHFP | |
1 | 4 | |
2 | 5 | use React\Dns\Config\Config; |
3 | 6 | use React\Dns\Resolver\Factory; |
7 | 7 | require __DIR__ . '/../vendor/autoload.php'; |
8 | 8 | |
9 | 9 | $loop = Factory::create(); |
10 | $executor = new UdpTransportExecutor($loop); | |
10 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
11 | 11 | |
12 | 12 | $name = isset($argv[1]) ? $argv[1] : 'www.google.com'; |
13 | 13 | |
14 | 14 | $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN); |
15 | 15 | $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN); |
16 | 16 | |
17 | $executor->query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) { | |
17 | $executor->query($ipv4Query)->then(function (Message $message) { | |
18 | 18 | foreach ($message->answers as $answer) { |
19 | 19 | echo 'IPv4: ' . $answer->data . PHP_EOL; |
20 | 20 | } |
21 | 21 | }, 'printf'); |
22 | $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) { | |
22 | $executor->query($ipv6Query)->then(function (Message $message) { | |
23 | 23 | foreach ($message->answers as $answer) { |
24 | 24 | echo 'IPv6: ' . $answer->data . PHP_EOL; |
25 | 25 | } |
0 | 0 | <?php |
1 | ||
2 | // $ php examples/92-query-any.php mailbox.org | |
3 | // $ php examples/92-query-any.php _carddav._tcp.mailbox.org | |
1 | 4 | |
2 | 5 | use React\Dns\Model\Message; |
3 | 6 | use React\Dns\Model\Record; |
4 | 7 | use React\Dns\Query\Query; |
5 | use React\Dns\Query\UdpTransportExecutor; | |
8 | use React\Dns\Query\TcpTransportExecutor; | |
6 | 9 | use React\EventLoop\Factory; |
7 | 10 | |
8 | 11 | require __DIR__ . '/../vendor/autoload.php'; |
9 | 12 | |
10 | 13 | $loop = Factory::create(); |
11 | $executor = new UdpTransportExecutor($loop); | |
14 | $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
12 | 15 | |
13 | 16 | $name = isset($argv[1]) ? $argv[1] : 'google.com'; |
14 | 17 | |
15 | 18 | $any = new Query($name, Message::TYPE_ANY, Message::CLASS_IN); |
16 | 19 | |
17 | $executor->query('8.8.8.8:53', $any)->then(function (Message $message) { | |
20 | $executor->query($any)->then(function (Message $message) { | |
18 | 21 | foreach ($message->answers as $answer) { |
19 | 22 | /* @var $answer Record */ |
20 | 23 | |
48 | 51 | $data = implode(' ', $data); |
49 | 52 | break; |
50 | 53 | case Message::TYPE_SRV: |
51 | // SRV records contains priority, weight, port and target, dump structure here | |
54 | // SRV records contain priority, weight, port and target, dump structure here | |
52 | 55 | $type = 'SRV'; |
53 | 56 | $data = json_encode($data); |
57 | break; | |
58 | case Message::TYPE_SSHFP: | |
59 | // SSHFP records contain algorithm, fingerprint type and hex fingerprint value | |
60 | $type = 'SSHFP'; | |
61 | $data = implode(' ', $data); | |
54 | 62 | break; |
55 | 63 | case Message::TYPE_SOA: |
56 | 64 | // SOA records contain structured data, dump structure here |
57 | 65 | $type = 'SOA'; |
58 | 66 | $data = json_encode($data); |
67 | break; | |
68 | case Message::TYPE_CAA: | |
69 | // CAA records contains flag, tag and value | |
70 | $type = 'CAA'; | |
71 | $data = $data['flag'] . ' ' . $data['tag'] . ' "' . $data['value'] . '"'; | |
59 | 72 | break; |
60 | 73 | default: |
61 | 74 | // unknown type uses HEX format |
7 | 7 | convertWarningsToExceptions="true" |
8 | 8 | processIsolation="false" |
9 | 9 | stopOnFailure="false" |
10 | syntaxCheck="false" | |
11 | 10 | bootstrap="vendor/autoload.php" |
12 | 11 | > |
13 | 12 | <testsuites> |
1 | 1 | |
2 | 2 | namespace React\Dns; |
3 | 3 | |
4 | class BadServerException extends \Exception | |
4 | final class BadServerException extends \Exception | |
5 | 5 | { |
6 | 6 | } |
3 | 3 | |
4 | 4 | use RuntimeException; |
5 | 5 | |
6 | class Config | |
6 | final class Config | |
7 | 7 | { |
8 | 8 | /** |
9 | 9 | * Loads the system DNS configuration |
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 | } |
71 | 71 | |
72 | 72 | return new self($contents); |
73 | 73 | } |
74 | ||
75 | private $contents; | |
74 | 76 | |
75 | 77 | /** |
76 | 78 | * Instantiate new hosts file with the given hosts file contents |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Model; | |
3 | ||
4 | class HeaderBag | |
5 | { | |
6 | public $attributes = array( | |
7 | 'qdCount' => 0, | |
8 | 'anCount' => 0, | |
9 | 'nsCount' => 0, | |
10 | 'arCount' => 0, | |
11 | 'qr' => 0, | |
12 | 'opcode' => Message::OPCODE_QUERY, | |
13 | 'aa' => 0, | |
14 | 'tc' => 0, | |
15 | 'rd' => 0, | |
16 | 'ra' => 0, | |
17 | 'z' => 0, | |
18 | 'rcode' => Message::RCODE_OK, | |
19 | ); | |
20 | ||
21 | /** | |
22 | * @deprecated unused, exists for BC only | |
23 | */ | |
24 | public $data = ''; | |
25 | ||
26 | public function get($name) | |
27 | { | |
28 | return isset($this->attributes[$name]) ? $this->attributes[$name] : null; | |
29 | } | |
30 | ||
31 | public function set($name, $value) | |
32 | { | |
33 | $this->attributes[$name] = $value; | |
34 | } | |
35 | ||
36 | public function isQuery() | |
37 | { | |
38 | return 0 === $this->attributes['qr']; | |
39 | } | |
40 | ||
41 | public function isResponse() | |
42 | { | |
43 | return 1 === $this->attributes['qr']; | |
44 | } | |
45 | ||
46 | public function isTruncated() | |
47 | { | |
48 | return 1 === $this->attributes['tc']; | |
49 | } | |
50 | ||
51 | public function populateCounts(Message $message) | |
52 | { | |
53 | $this->attributes['qdCount'] = count($message->questions); | |
54 | $this->attributes['anCount'] = count($message->answers); | |
55 | $this->attributes['nsCount'] = count($message->authority); | |
56 | $this->attributes['arCount'] = count($message->additional); | |
57 | } | |
58 | } |
3 | 3 | |
4 | 4 | use React\Dns\Query\Query; |
5 | 5 | |
6 | class Message | |
6 | /** | |
7 | * This class represents an outgoing query message or an incoming response message | |
8 | * | |
9 | * @link https://tools.ietf.org/html/rfc1035#section-4.1.1 | |
10 | */ | |
11 | final class Message | |
7 | 12 | { |
8 | 13 | const TYPE_A = 1; |
9 | 14 | const TYPE_NS = 2; |
14 | 19 | const TYPE_TXT = 16; |
15 | 20 | const TYPE_AAAA = 28; |
16 | 21 | const TYPE_SRV = 33; |
22 | const TYPE_SSHFP = 44; | |
17 | 23 | const TYPE_ANY = 255; |
24 | const TYPE_CAA = 257; | |
18 | 25 | |
19 | 26 | const CLASS_IN = 1; |
20 | 27 | |
38 | 45 | public static function createRequestForQuery(Query $query) |
39 | 46 | { |
40 | 47 | $request = new Message(); |
41 | $request->header->set('id', self::generateId()); | |
42 | $request->header->set('rd', 1); | |
43 | $request->questions[] = (array) $query; | |
44 | $request->prepare(); | |
48 | $request->id = self::generateId(); | |
49 | $request->rd = true; | |
50 | $request->questions[] = $query; | |
45 | 51 | |
46 | 52 | return $request; |
47 | 53 | } |
56 | 62 | public static function createResponseWithAnswersForQuery(Query $query, array $answers) |
57 | 63 | { |
58 | 64 | $response = new Message(); |
59 | $response->header->set('id', self::generateId()); | |
60 | $response->header->set('qr', 1); | |
61 | $response->header->set('opcode', Message::OPCODE_QUERY); | |
62 | $response->header->set('rd', 1); | |
63 | $response->header->set('rcode', Message::RCODE_OK); | |
65 | $response->id = self::generateId(); | |
66 | $response->qr = true; | |
67 | $response->rd = true; | |
64 | 68 | |
65 | $response->questions[] = (array) $query; | |
69 | $response->questions[] = $query; | |
66 | 70 | |
67 | 71 | foreach ($answers as $record) { |
68 | 72 | $response->answers[] = $record; |
69 | 73 | } |
70 | ||
71 | $response->prepare(); | |
72 | 74 | |
73 | 75 | return $response; |
74 | 76 | } |
99 | 101 | return mt_rand(0, 0xffff); |
100 | 102 | } |
101 | 103 | |
102 | public $header; | |
103 | public $questions = array(); | |
104 | public $answers = array(); | |
105 | public $authority = array(); | |
106 | public $additional = array(); | |
107 | ||
108 | 104 | /** |
109 | * @deprecated still used internally for BC reasons, should not be used externally. | |
110 | */ | |
111 | public $data = ''; | |
112 | ||
113 | /** | |
114 | * @deprecated still used internally for BC reasons, should not be used externally. | |
115 | */ | |
116 | public $consumed = 0; | |
117 | ||
118 | public function __construct() | |
119 | { | |
120 | $this->header = new HeaderBag(); | |
121 | } | |
122 | ||
123 | /** | |
124 | * Returns the 16 bit message ID | |
105 | * The 16 bit message ID | |
125 | 106 | * |
126 | 107 | * The response message ID has to match the request message ID. This allows |
127 | 108 | * the receiver to verify this is the correct response message. An outside |
128 | 109 | * attacker may try to inject fake responses by "guessing" the message ID, |
129 | 110 | * so this should use a proper CSPRNG to avoid possible cache poisoning. |
130 | 111 | * |
131 | * @return int | |
112 | * @var int 16 bit message ID | |
132 | 113 | * @see self::generateId() |
133 | 114 | */ |
134 | public function getId() | |
135 | { | |
136 | return $this->header->get('id'); | |
137 | } | |
115 | public $id = 0; | |
138 | 116 | |
139 | 117 | /** |
140 | * Returns the response code (RCODE) | |
118 | * @var bool Query/Response flag, query=false or response=true | |
119 | */ | |
120 | public $qr = false; | |
121 | ||
122 | /** | |
123 | * @var int specifies the kind of query (4 bit), see self::OPCODE_* constants | |
124 | * @see self::OPCODE_QUERY | |
125 | */ | |
126 | public $opcode = self::OPCODE_QUERY; | |
127 | ||
128 | /** | |
141 | 129 | * |
142 | * @return int see self::RCODE_* constants | |
130 | * @var bool Authoritative Answer | |
143 | 131 | */ |
144 | public function getResponseCode() | |
145 | { | |
146 | return $this->header->get('rcode'); | |
147 | } | |
132 | public $aa = false; | |
148 | 133 | |
149 | public function prepare() | |
150 | { | |
151 | $this->header->populateCounts($this); | |
152 | } | |
134 | /** | |
135 | * @var bool TrunCation | |
136 | */ | |
137 | public $tc = false; | |
138 | ||
139 | /** | |
140 | * @var bool Recursion Desired | |
141 | */ | |
142 | public $rd = false; | |
143 | ||
144 | /** | |
145 | * @var bool Recursion Available | |
146 | */ | |
147 | public $ra = false; | |
148 | ||
149 | /** | |
150 | * @var int response code (4 bit), see self::RCODE_* constants | |
151 | * @see self::RCODE_OK | |
152 | */ | |
153 | public $rcode = Message::RCODE_OK; | |
154 | ||
155 | /** | |
156 | * An array of Query objects | |
157 | * | |
158 | * ```php | |
159 | * $questions = array( | |
160 | * new Query( | |
161 | * 'reactphp.org', | |
162 | * Message::TYPE_A, | |
163 | * Message::CLASS_IN | |
164 | * ) | |
165 | * ); | |
166 | * ``` | |
167 | * | |
168 | * @var Query[] | |
169 | */ | |
170 | public $questions = array(); | |
171 | ||
172 | /** | |
173 | * @var Record[] | |
174 | */ | |
175 | public $answers = array(); | |
176 | ||
177 | /** | |
178 | * @var Record[] | |
179 | */ | |
180 | public $authority = array(); | |
181 | ||
182 | /** | |
183 | * @var Record[] | |
184 | */ | |
185 | public $additional = array(); | |
153 | 186 | } |
1 | 1 | |
2 | 2 | namespace React\Dns\Model; |
3 | 3 | |
4 | class Record | |
4 | /** | |
5 | * This class represents a single resulting record in a response message | |
6 | * | |
7 | * It uses a structure similar to `\React\Dns\Query\Query`, but does include | |
8 | * fields for resulting TTL and resulting record data (IPs etc.). | |
9 | * | |
10 | * @link https://tools.ietf.org/html/rfc1035#section-4.1.3 | |
11 | * @see \React\Dns\Query\Query | |
12 | */ | |
13 | final class Record | |
5 | 14 | { |
6 | 15 | /** |
7 | 16 | * @var string hostname without trailing dot, for example "reactphp.org" |
33 | 42 | * |
34 | 43 | * - A: |
35 | 44 | * IPv4 address string, for example "192.168.1.1". |
45 | * | |
36 | 46 | * - AAAA: |
37 | 47 | * IPv6 address string, for example "::1". |
48 | * | |
38 | 49 | * - CNAME / PTR / NS: |
39 | 50 | * The hostname without trailing dot, for example "reactphp.org". |
51 | * | |
40 | 52 | * - TXT: |
41 | 53 | * List of string values, for example `["v=spf1 include:example.com"]`. |
42 | 54 | * This is commonly a list with only a single string value, but this |
48 | 60 | * suggests using key-value pairs such as `["name=test","version=1"]`, but |
49 | 61 | * interpretation of this is not enforced and left up to consumers of this |
50 | 62 | * library (used for DNS-SD/Zeroconf and others). |
63 | * | |
51 | 64 | * - MX: |
52 | 65 | * Mail server priority (UINT16) and target hostname without trailing dot, |
53 | 66 | * for example `{"priority":10,"target":"mx.example.com"}`. |
56 | 69 | * referred to as exchange). If a response message contains multiple |
57 | 70 | * records of this type, targets should be sorted by priority (lowest |
58 | 71 | * first) - this is left up to consumers of this library (used for SMTP). |
72 | * | |
59 | 73 | * - SRV: |
60 | 74 | * Service priority (UINT16), service weight (UINT16), service port (UINT16) |
61 | 75 | * and target hostname without trailing dot, for example |
68 | 82 | * randomly according to their weight - this is left up to consumers of |
69 | 83 | * this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782) |
70 | 84 | * for more details. |
85 | * | |
86 | * - SSHFP: | |
87 | * Includes algorithm (UNIT8), fingerprint type (UNIT8) and fingerprint | |
88 | * value as lower case hex string, for example: | |
89 | * `{"algorithm":1,"type":1,"fingerprint":"0123456789abcdef..."}` | |
90 | * See also https://www.iana.org/assignments/dns-sshfp-rr-parameters/dns-sshfp-rr-parameters.xhtml | |
91 | * for algorithm and fingerprint type assignments. | |
92 | * | |
71 | 93 | * - SOA: |
72 | 94 | * Includes master hostname without trailing dot, responsible person email |
73 | 95 | * as hostname without trailing dot and serial, refresh, retry, expire and |
74 | 96 | * minimum times in seconds (UINT32 each), for example: |
75 | 97 | * `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial": |
76 | 98 | * 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`. |
99 | * | |
100 | * - CAA: | |
101 | * Includes flag (UNIT8), tag string and value string, for example: | |
102 | * `{"flag":128,"tag":"issue","value":"letsencrypt.org"}` | |
103 | * | |
77 | 104 | * - Any other unknown type: |
78 | 105 | * An opaque binary string containing the RDATA as transported in the DNS |
79 | 106 | * record. For forwards compatibility, you should not rely on this format |
86 | 113 | */ |
87 | 114 | public $data; |
88 | 115 | |
89 | public function __construct($name, $type, $class, $ttl = 0, $data = null) | |
116 | /** | |
117 | * @param string $name | |
118 | * @param int $type | |
119 | * @param int $class | |
120 | * @param int $ttl | |
121 | * @param string|string[]|array $data | |
122 | */ | |
123 | public function __construct($name, $type, $class, $ttl, $data) | |
90 | 124 | { |
91 | 125 | $this->name = $name; |
92 | 126 | $this->type = $type; |
2 | 2 | namespace React\Dns\Protocol; |
3 | 3 | |
4 | 4 | use React\Dns\Model\Message; |
5 | use React\Dns\Model\HeaderBag; | |
5 | use React\Dns\Model\Record; | |
6 | use React\Dns\Query\Query; | |
6 | 7 | |
7 | class BinaryDumper | |
8 | final class BinaryDumper | |
8 | 9 | { |
10 | /** | |
11 | * @param Message $message | |
12 | * @return string | |
13 | */ | |
9 | 14 | public function toBinary(Message $message) |
10 | 15 | { |
11 | 16 | $data = ''; |
12 | 17 | |
13 | $data .= $this->headerToBinary($message->header); | |
18 | $data .= $this->headerToBinary($message); | |
14 | 19 | $data .= $this->questionToBinary($message->questions); |
20 | $data .= $this->recordsToBinary($message->answers); | |
21 | $data .= $this->recordsToBinary($message->authority); | |
22 | $data .= $this->recordsToBinary($message->additional); | |
15 | 23 | |
16 | 24 | return $data; |
17 | 25 | } |
18 | 26 | |
19 | private function headerToBinary(HeaderBag $header) | |
27 | /** | |
28 | * @param Message $message | |
29 | * @return string | |
30 | */ | |
31 | private function headerToBinary(Message $message) | |
20 | 32 | { |
21 | 33 | $data = ''; |
22 | 34 | |
23 | $data .= pack('n', $header->get('id')); | |
35 | $data .= pack('n', $message->id); | |
24 | 36 | |
25 | 37 | $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'); | |
38 | $flags = ($flags << 1) | ($message->qr ? 1 : 0); | |
39 | $flags = ($flags << 4) | $message->opcode; | |
40 | $flags = ($flags << 1) | ($message->aa ? 1 : 0); | |
41 | $flags = ($flags << 1) | ($message->tc ? 1 : 0); | |
42 | $flags = ($flags << 1) | ($message->rd ? 1 : 0); | |
43 | $flags = ($flags << 1) | ($message->ra ? 1 : 0); | |
44 | $flags = ($flags << 3) | 0; // skip unused zero bit | |
45 | $flags = ($flags << 4) | $message->rcode; | |
34 | 46 | |
35 | 47 | $data .= pack('n', $flags); |
36 | 48 | |
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')); | |
49 | $data .= pack('n', count($message->questions)); | |
50 | $data .= pack('n', count($message->answers)); | |
51 | $data .= pack('n', count($message->authority)); | |
52 | $data .= pack('n', count($message->additional)); | |
41 | 53 | |
42 | 54 | return $data; |
43 | 55 | } |
44 | 56 | |
57 | /** | |
58 | * @param Query[] $questions | |
59 | * @return string | |
60 | */ | |
45 | 61 | private function questionToBinary(array $questions) |
46 | 62 | { |
47 | 63 | $data = ''; |
48 | 64 | |
49 | 65 | 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']); | |
66 | $data .= $this->domainNameToBinary($question->name); | |
67 | $data .= pack('n*', $question->type, $question->class); | |
57 | 68 | } |
58 | 69 | |
59 | 70 | return $data; |
60 | 71 | } |
72 | ||
73 | /** | |
74 | * @param Record[] $records | |
75 | * @return string | |
76 | */ | |
77 | private function recordsToBinary(array $records) | |
78 | { | |
79 | $data = ''; | |
80 | ||
81 | foreach ($records as $record) { | |
82 | /* @var $record Record */ | |
83 | switch ($record->type) { | |
84 | case Message::TYPE_A: | |
85 | case Message::TYPE_AAAA: | |
86 | $binary = \inet_pton($record->data); | |
87 | break; | |
88 | case Message::TYPE_CNAME: | |
89 | case Message::TYPE_NS: | |
90 | case Message::TYPE_PTR: | |
91 | $binary = $this->domainNameToBinary($record->data); | |
92 | break; | |
93 | case Message::TYPE_TXT: | |
94 | $binary = $this->textsToBinary($record->data); | |
95 | break; | |
96 | case Message::TYPE_MX: | |
97 | $binary = \pack( | |
98 | 'n', | |
99 | $record->data['priority'] | |
100 | ); | |
101 | $binary .= $this->domainNameToBinary($record->data['target']); | |
102 | break; | |
103 | case Message::TYPE_SRV: | |
104 | $binary = \pack( | |
105 | 'n*', | |
106 | $record->data['priority'], | |
107 | $record->data['weight'], | |
108 | $record->data['port'] | |
109 | ); | |
110 | $binary .= $this->domainNameToBinary($record->data['target']); | |
111 | break; | |
112 | case Message::TYPE_SOA: | |
113 | $binary = $this->domainNameToBinary($record->data['mname']); | |
114 | $binary .= $this->domainNameToBinary($record->data['rname']); | |
115 | $binary .= \pack( | |
116 | 'N*', | |
117 | $record->data['serial'], | |
118 | $record->data['refresh'], | |
119 | $record->data['retry'], | |
120 | $record->data['expire'], | |
121 | $record->data['minimum'] | |
122 | ); | |
123 | break; | |
124 | case Message::TYPE_CAA: | |
125 | $binary = \pack( | |
126 | 'C*', | |
127 | $record->data['flag'], | |
128 | \strlen($record->data['tag']) | |
129 | ); | |
130 | $binary .= $record->data['tag']; | |
131 | $binary .= $record->data['value']; | |
132 | break; | |
133 | case Message::TYPE_SSHFP: | |
134 | $binary = \pack( | |
135 | 'CCH*', | |
136 | $record->data['algorithm'], | |
137 | $record->data['type'], | |
138 | $record->data['fingerprint'] | |
139 | ); | |
140 | break; | |
141 | default: | |
142 | // RDATA is already stored as binary value for unknown record types | |
143 | $binary = $record->data; | |
144 | } | |
145 | ||
146 | $data .= $this->domainNameToBinary($record->name); | |
147 | $data .= \pack('nnNn', $record->type, $record->class, $record->ttl, \strlen($binary)); | |
148 | $data .= $binary; | |
149 | } | |
150 | ||
151 | return $data; | |
152 | } | |
153 | ||
154 | /** | |
155 | * @param string[] $texts | |
156 | * @return string | |
157 | */ | |
158 | private function textsToBinary(array $texts) | |
159 | { | |
160 | $data = ''; | |
161 | foreach ($texts as $text) { | |
162 | $data .= \chr(\strlen($text)) . $text; | |
163 | } | |
164 | return $data; | |
165 | } | |
166 | ||
167 | /** | |
168 | * @param string $host | |
169 | * @return string | |
170 | */ | |
171 | private function domainNameToBinary($host) | |
172 | { | |
173 | if ($host === '') { | |
174 | return "\0"; | |
175 | } | |
176 | ||
177 | // break up domain name at each dot that is not preceeded by a backslash (escaped notation) | |
178 | return $this->textsToBinary( | |
179 | \array_map( | |
180 | 'stripcslashes', | |
181 | \preg_split( | |
182 | '/(?<!\\\\)\./', | |
183 | $host . '.' | |
184 | ) | |
185 | ) | |
186 | ); | |
187 | } | |
61 | 188 | } |
3 | 3 | |
4 | 4 | use React\Dns\Model\Message; |
5 | 5 | use React\Dns\Model\Record; |
6 | use React\Dns\Query\Query; | |
6 | 7 | use InvalidArgumentException; |
7 | 8 | |
8 | 9 | /** |
10 | 11 | * |
11 | 12 | * Obsolete and uncommon types and classes are not implemented. |
12 | 13 | */ |
13 | class Parser | |
14 | final class Parser | |
14 | 15 | { |
15 | 16 | /** |
16 | 17 | * Parses the given raw binary message into a Message object |
21 | 22 | */ |
22 | 23 | public function parseMessage($data) |
23 | 24 | { |
25 | // create empty message with two additional, temporary properties for parser | |
24 | 26 | $message = new Message(); |
27 | $message->data = $data; | |
28 | $message->consumed = null; | |
29 | ||
25 | 30 | if ($this->parse($data, $message) !== $message) { |
26 | 31 | throw new InvalidArgumentException('Unable to parse binary message'); |
27 | 32 | } |
28 | 33 | |
34 | unset($message->data, $message->consumed); | |
35 | ||
29 | 36 | return $message; |
30 | 37 | } |
31 | 38 | |
32 | /** | |
33 | * @deprecated unused, exists for BC only | |
34 | * @codeCoverageIgnore | |
35 | */ | |
36 | public function parseChunk($data, Message $message) | |
37 | { | |
38 | return $this->parse($data, $message); | |
39 | } | |
40 | ||
41 | 39 | private function parse($data, Message $message) |
42 | { | |
43 | $message->data .= $data; | |
44 | ||
45 | if (!$message->header->get('id')) { | |
46 | if (!$this->parseHeader($message)) { | |
47 | return; | |
48 | } | |
49 | } | |
50 | ||
51 | if ($message->header->get('qdCount') != count($message->questions)) { | |
52 | if (!$this->parseQuestion($message)) { | |
53 | return; | |
54 | } | |
55 | } | |
56 | ||
57 | if ($message->header->get('anCount') != count($message->answers)) { | |
58 | if (!$this->parseAnswer($message)) { | |
59 | return; | |
60 | } | |
61 | } | |
62 | ||
63 | return $message; | |
64 | } | |
65 | ||
66 | public function parseHeader(Message $message) | |
67 | 40 | { |
68 | 41 | if (!isset($message->data[12 - 1])) { |
69 | 42 | return; |
70 | 43 | } |
71 | 44 | |
72 | $header = substr($message->data, 0, 12); | |
45 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($message->data, 0, 12))); | |
73 | 46 | $message->consumed += 12; |
74 | 47 | |
75 | list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', $header)); | |
76 | ||
77 | $rcode = $fields & bindec('1111'); | |
78 | $z = ($fields >> 4) & bindec('111'); | |
79 | $ra = ($fields >> 7) & 1; | |
80 | $rd = ($fields >> 8) & 1; | |
81 | $tc = ($fields >> 9) & 1; | |
82 | $aa = ($fields >> 10) & 1; | |
83 | $opcode = ($fields >> 11) & bindec('1111'); | |
84 | $qr = ($fields >> 15) & 1; | |
85 | ||
86 | $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', | |
87 | 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); | |
88 | ||
89 | ||
90 | foreach ($vars as $name => $value) { | |
91 | $message->header->set($name, $value); | |
48 | $message->id = $id; | |
49 | $message->rcode = $fields & 0xf; | |
50 | $message->ra = (($fields >> 7) & 1) === 1; | |
51 | $message->rd = (($fields >> 8) & 1) === 1; | |
52 | $message->tc = (($fields >> 9) & 1) === 1; | |
53 | $message->aa = (($fields >> 10) & 1) === 1; | |
54 | $message->opcode = ($fields >> 11) & 0xf; | |
55 | $message->qr = (($fields >> 15) & 1) === 1; | |
56 | ||
57 | // parse all questions | |
58 | for ($i = $qdCount; $i > 0; --$i) { | |
59 | $question = $this->parseQuestion($message); | |
60 | if ($question === null) { | |
61 | return; | |
62 | } else { | |
63 | $message->questions[] = $question; | |
64 | } | |
65 | } | |
66 | ||
67 | // parse all answer records | |
68 | for ($i = $anCount; $i > 0; --$i) { | |
69 | $record = $this->parseRecord($message); | |
70 | if ($record === null) { | |
71 | return; | |
72 | } else { | |
73 | $message->answers[] = $record; | |
74 | } | |
75 | } | |
76 | ||
77 | // parse all authority records | |
78 | for ($i = $nsCount; $i > 0; --$i) { | |
79 | $record = $this->parseRecord($message); | |
80 | if ($record === null) { | |
81 | return; | |
82 | } else { | |
83 | $message->authority[] = $record; | |
84 | } | |
85 | } | |
86 | ||
87 | // parse all additional records | |
88 | for ($i = $arCount; $i > 0; --$i) { | |
89 | $record = $this->parseRecord($message); | |
90 | if ($record === null) { | |
91 | return; | |
92 | } else { | |
93 | $message->additional[] = $record; | |
94 | } | |
92 | 95 | } |
93 | 96 | |
94 | 97 | return $message; |
95 | 98 | } |
96 | 99 | |
97 | public function parseQuestion(Message $message) | |
100 | /** | |
101 | * @param Message $message | |
102 | * @return ?Query | |
103 | */ | |
104 | private function parseQuestion(Message $message) | |
98 | 105 | { |
99 | 106 | $consumed = $message->consumed; |
100 | 107 | |
109 | 116 | |
110 | 117 | $message->consumed = $consumed; |
111 | 118 | |
112 | $message->questions[] = array( | |
113 | 'name' => implode('.', $labels), | |
114 | 'type' => $type, | |
115 | 'class' => $class, | |
119 | return new Query( | |
120 | implode('.', $labels), | |
121 | $type, | |
122 | $class | |
116 | 123 | ); |
117 | ||
118 | if ($message->header->get('qdCount') != count($message->questions)) { | |
119 | return $this->parseQuestion($message); | |
120 | } | |
121 | ||
122 | return $message; | |
123 | } | |
124 | ||
125 | public function parseAnswer(Message $message) | |
124 | } | |
125 | ||
126 | /** | |
127 | * @param Message $message | |
128 | * @return ?Record returns parsed Record on success or null if data is invalid/incomplete | |
129 | */ | |
130 | private function parseRecord(Message $message) | |
126 | 131 | { |
127 | 132 | $consumed = $message->consumed; |
128 | 133 | |
129 | 134 | list($name, $consumed) = $this->readDomain($message->data, $consumed); |
130 | 135 | |
131 | 136 | if ($name === null || !isset($message->data[$consumed + 10 - 1])) { |
132 | return; | |
137 | return null; | |
133 | 138 | } |
134 | 139 | |
135 | 140 | list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); |
147 | 152 | $consumed += 2; |
148 | 153 | |
149 | 154 | if (!isset($message->data[$consumed + $rdLength - 1])) { |
150 | return; | |
155 | return null; | |
151 | 156 | } |
152 | 157 | |
153 | 158 | $rdata = null; |
192 | 197 | 'weight' => $weight, |
193 | 198 | 'port' => $port, |
194 | 199 | 'target' => $target |
200 | ); | |
201 | } | |
202 | } elseif (Message::TYPE_SSHFP === $type) { | |
203 | if ($rdLength > 2) { | |
204 | list($algorithm, $hash) = \array_values(\unpack('C*', \substr($message->data, $consumed, 2))); | |
205 | $fingerprint = \bin2hex(\substr($message->data, $consumed + 2, $rdLength - 2)); | |
206 | $consumed += $rdLength; | |
207 | ||
208 | $rdata = array( | |
209 | 'algorithm' => $algorithm, | |
210 | 'type' => $hash, | |
211 | 'fingerprint' => $fingerprint | |
195 | 212 | ); |
196 | 213 | } |
197 | 214 | } elseif (Message::TYPE_SOA === $type) { |
212 | 229 | 'minimum' => $minimum |
213 | 230 | ); |
214 | 231 | } |
232 | } elseif (Message::TYPE_CAA === $type) { | |
233 | if ($rdLength > 3) { | |
234 | list($flag, $tagLength) = array_values(unpack('C*', substr($message->data, $consumed, 2))); | |
235 | ||
236 | if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) { | |
237 | $tag = substr($message->data, $consumed + 2, $tagLength); | |
238 | $value = substr($message->data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength); | |
239 | $consumed += $rdLength; | |
240 | ||
241 | $rdata = array( | |
242 | 'flag' => $flag, | |
243 | 'tag' => $tag, | |
244 | 'value' => $value | |
245 | ); | |
246 | } | |
247 | } | |
215 | 248 | } else { |
216 | 249 | // unknown types simply parse rdata as an opaque binary string |
217 | 250 | $rdata = substr($message->data, $consumed, $rdLength); |
220 | 253 | |
221 | 254 | // ensure parsing record data consumes expact number of bytes indicated in record length |
222 | 255 | if ($consumed !== $expected || $rdata === null) { |
223 | return; | |
256 | return null; | |
224 | 257 | } |
225 | 258 | |
226 | 259 | $message->consumed = $consumed; |
227 | 260 | |
228 | $record = new Record($name, $type, $class, $ttl, $rdata); | |
229 | ||
230 | $message->answers[] = $record; | |
231 | ||
232 | if ($message->header->get('anCount') != count($message->answers)) { | |
233 | return $this->parseAnswer($message); | |
234 | } | |
235 | ||
236 | return $message; | |
261 | return new Record($name, $type, $class, $ttl, $rdata); | |
237 | 262 | } |
238 | 263 | |
239 | 264 | private function readDomain($data, $consumed) |
244 | 269 | return array(null, null); |
245 | 270 | } |
246 | 271 | |
247 | return array(implode('.', $labels), $consumed); | |
272 | // use escaped notation for each label part, then join using dots | |
273 | return array( | |
274 | \implode( | |
275 | '.', | |
276 | \array_map( | |
277 | function ($label) { | |
278 | return \addcslashes($label, "\0..\40.\177"); | |
279 | }, | |
280 | $labels | |
281 | ) | |
282 | ), | |
283 | $consumed | |
284 | ); | |
248 | 285 | } |
249 | 286 | |
250 | 287 | private function readLabels($data, $consumed) |
293 | 330 | |
294 | 331 | return array($labels, $consumed); |
295 | 332 | } |
296 | ||
297 | /** | |
298 | * @deprecated unused, exists for BC only | |
299 | * @codeCoverageIgnore | |
300 | */ | |
301 | public function isEndOfLabels($data, $consumed) | |
302 | { | |
303 | $length = ord(substr($data, $consumed, 1)); | |
304 | return 0 === $length; | |
305 | } | |
306 | ||
307 | /** | |
308 | * @deprecated unused, exists for BC only | |
309 | * @codeCoverageIgnore | |
310 | */ | |
311 | public function getCompressedLabel($data, $consumed) | |
312 | { | |
313 | list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); | |
314 | list($labels) = $this->readLabels($data, $nameOffset); | |
315 | ||
316 | return array($labels, $consumed); | |
317 | } | |
318 | ||
319 | /** | |
320 | * @deprecated unused, exists for BC only | |
321 | * @codeCoverageIgnore | |
322 | */ | |
323 | public function isCompressedLabel($data, $consumed) | |
324 | { | |
325 | $mask = 0xc000; // 1100000000000000 | |
326 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); | |
327 | ||
328 | return (bool) ($peek & $mask); | |
329 | } | |
330 | ||
331 | /** | |
332 | * @deprecated unused, exists for BC only | |
333 | * @codeCoverageIgnore | |
334 | */ | |
335 | public function getCompressedLabelOffset($data, $consumed) | |
336 | { | |
337 | $mask = 0x3fff; // 0011111111111111 | |
338 | list($peek) = array_values(unpack('n', substr($data, $consumed, 2))); | |
339 | ||
340 | return array($peek & $mask, $consumed + 2); | |
341 | } | |
342 | ||
343 | /** | |
344 | * @deprecated unused, exists for BC only | |
345 | * @codeCoverageIgnore | |
346 | */ | |
347 | public function signedLongToUnsignedLong($i) | |
348 | { | |
349 | return $i & 0x80000000 ? $i - 0xffffffff : $i; | |
350 | } | |
351 | 333 | } |
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 | use React\Cache\CacheInterface; | |
5 | use React\Dns\Model\Message; | |
6 | use React\Promise\Promise; | |
7 | ||
8 | final class CachingExecutor implements ExecutorInterface | |
9 | { | |
10 | /** | |
11 | * Default TTL for negative responses (NXDOMAIN etc.). | |
12 | * | |
13 | * @internal | |
14 | */ | |
15 | const TTL = 60; | |
16 | ||
17 | private $executor; | |
18 | private $cache; | |
19 | ||
20 | public function __construct(ExecutorInterface $executor, CacheInterface $cache) | |
21 | { | |
22 | $this->executor = $executor; | |
23 | $this->cache = $cache; | |
24 | } | |
25 | ||
26 | public function query(Query $query) | |
27 | { | |
28 | $id = $query->name . ':' . $query->type . ':' . $query->class; | |
29 | $cache = $this->cache; | |
30 | $that = $this; | |
31 | $executor = $this->executor; | |
32 | ||
33 | $pending = $cache->get($id); | |
34 | return new Promise(function ($resolve, $reject) use ($query, $id, $cache, $executor, &$pending, $that) { | |
35 | $pending->then( | |
36 | function ($message) use ($query, $id, $cache, $executor, &$pending, $that) { | |
37 | // return cached response message on cache hit | |
38 | if ($message !== null) { | |
39 | return $message; | |
40 | } | |
41 | ||
42 | // perform DNS lookup if not already cached | |
43 | return $pending = $executor->query($query)->then( | |
44 | function (Message $message) use ($cache, $id, $that) { | |
45 | // DNS response message received => store in cache when not truncated and return | |
46 | if (!$message->tc) { | |
47 | $cache->set($id, $message, $that->ttl($message)); | |
48 | } | |
49 | ||
50 | return $message; | |
51 | } | |
52 | ); | |
53 | } | |
54 | )->then($resolve, function ($e) use ($reject, &$pending) { | |
55 | $reject($e); | |
56 | $pending = null; | |
57 | }); | |
58 | }, function ($_, $reject) use (&$pending, $query) { | |
59 | $reject(new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled')); | |
60 | $pending->cancel(); | |
61 | $pending = null; | |
62 | }); | |
63 | } | |
64 | ||
65 | /** | |
66 | * @param Message $message | |
67 | * @return int | |
68 | * @internal | |
69 | */ | |
70 | public function ttl(Message $message) | |
71 | { | |
72 | // select TTL from answers (should all be the same), use smallest value if available | |
73 | // @link https://tools.ietf.org/html/rfc2181#section-5.2 | |
74 | $ttl = null; | |
75 | foreach ($message->answers as $answer) { | |
76 | if ($ttl === null || $answer->ttl < $ttl) { | |
77 | $ttl = $answer->ttl; | |
78 | } | |
79 | } | |
80 | ||
81 | if ($ttl === null) { | |
82 | $ttl = self::TTL; | |
83 | } | |
84 | ||
85 | return $ttl; | |
86 | } | |
87 | } |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | class CancellationException extends \RuntimeException | |
4 | final class CancellationException extends \RuntimeException | |
5 | 5 | { |
6 | 6 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Promise\Promise; | |
5 | ||
6 | /** | |
7 | * Cooperatively resolves hosts via the given base executor to ensure same query is not run concurrently | |
8 | * | |
9 | * Wraps an existing `ExecutorInterface` to keep tracking of pending queries | |
10 | * and only starts a new query when the same query is not already pending. Once | |
11 | * the underlying query is fulfilled/rejected, it will forward its value to all | |
12 | * promises awaiting the same query. | |
13 | * | |
14 | * This means it will not limit concurrency for queries that differ, for example | |
15 | * when sending many queries for different host names or types. | |
16 | * | |
17 | * This is useful because all executors are entirely async and as such allow you | |
18 | * to execute any number of queries concurrently. You should probably limit the | |
19 | * number of concurrent queries in your application or you're very likely going | |
20 | * to face rate limitations and bans on the resolver end. For many common | |
21 | * applications, you may want to avoid sending the same query multiple times | |
22 | * when the first one is still pending, so you will likely want to use this in | |
23 | * combination with some other executor like this: | |
24 | * | |
25 | * ```php | |
26 | * $executor = new CoopExecutor( | |
27 | * new RetryExecutor( | |
28 | * new TimeoutExecutor( | |
29 | * new UdpTransportExecutor($nameserver, $loop), | |
30 | * 3.0, | |
31 | * $loop | |
32 | * ) | |
33 | * ) | |
34 | * ); | |
35 | * ``` | |
36 | */ | |
37 | final class CoopExecutor implements ExecutorInterface | |
38 | { | |
39 | private $executor; | |
40 | private $pending = array(); | |
41 | private $counts = array(); | |
42 | ||
43 | public function __construct(ExecutorInterface $base) | |
44 | { | |
45 | $this->executor = $base; | |
46 | } | |
47 | ||
48 | public function query(Query $query) | |
49 | { | |
50 | $key = $this->serializeQueryToIdentity($query); | |
51 | if (isset($this->pending[$key])) { | |
52 | // same query is already pending, so use shared reference to pending query | |
53 | $promise = $this->pending[$key]; | |
54 | ++$this->counts[$key]; | |
55 | } else { | |
56 | // no such query pending, so start new query and keep reference until it's fulfilled or rejected | |
57 | $promise = $this->executor->query($query); | |
58 | $this->pending[$key] = $promise; | |
59 | $this->counts[$key] = 1; | |
60 | ||
61 | $pending =& $this->pending; | |
62 | $counts =& $this->counts; | |
63 | $promise->then(function () use ($key, &$pending, &$counts) { | |
64 | unset($pending[$key], $counts[$key]); | |
65 | }, function () use ($key, &$pending, &$counts) { | |
66 | unset($pending[$key], $counts[$key]); | |
67 | }); | |
68 | } | |
69 | ||
70 | // Return a child promise awaiting the pending query. | |
71 | // Cancelling this child promise should only cancel the pending query | |
72 | // when no other child promise is awaiting the same query. | |
73 | $pending =& $this->pending; | |
74 | $counts =& $this->counts; | |
75 | return new Promise(function ($resolve, $reject) use ($promise) { | |
76 | $promise->then($resolve, $reject); | |
77 | }, function () use (&$promise, $key, $query, &$pending, &$counts) { | |
78 | if (--$counts[$key] < 1) { | |
79 | unset($pending[$key], $counts[$key]); | |
80 | $promise->cancel(); | |
81 | $promise = null; | |
82 | } | |
83 | throw new \RuntimeException('DNS query for ' . $query->name . ' has been cancelled'); | |
84 | }); | |
85 | } | |
86 | ||
87 | private function serializeQueryToIdentity(Query $query) | |
88 | { | |
89 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); | |
90 | } | |
91 | } |
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 | /** | |
14 | * @deprecated unused, exists for BC only | |
15 | * @see UdpTransportExecutor | |
16 | */ | |
17 | class Executor implements ExecutorInterface | |
18 | { | |
19 | private $loop; | |
20 | private $parser; | |
21 | private $dumper; | |
22 | private $timeout; | |
23 | ||
24 | /** | |
25 | * | |
26 | * Note that albeit supported, the $timeout parameter is deprecated! | |
27 | * You should pass a `null` value here instead. If you need timeout handling, | |
28 | * use the `TimeoutConnector` instead. | |
29 | * | |
30 | * @param LoopInterface $loop | |
31 | * @param Parser $parser | |
32 | * @param BinaryDumper $dumper | |
33 | * @param null|float $timeout DEPRECATED: timeout for DNS query or NULL=no timeout | |
34 | */ | |
35 | public function __construct(LoopInterface $loop, Parser $parser, BinaryDumper $dumper, $timeout = 5) | |
36 | { | |
37 | $this->loop = $loop; | |
38 | $this->parser = $parser; | |
39 | $this->dumper = $dumper; | |
40 | $this->timeout = $timeout; | |
41 | } | |
42 | ||
43 | public function query($nameserver, Query $query) | |
44 | { | |
45 | $request = Message::createRequestForQuery($query); | |
46 | ||
47 | $queryData = $this->dumper->toBinary($request); | |
48 | $transport = strlen($queryData) > 512 ? 'tcp' : 'udp'; | |
49 | ||
50 | return $this->doQuery($nameserver, $transport, $queryData, $query->name); | |
51 | } | |
52 | ||
53 | /** | |
54 | * @deprecated unused, exists for BC only | |
55 | */ | |
56 | public function prepareRequest(Query $query) | |
57 | { | |
58 | return Message::createRequestForQuery($query); | |
59 | } | |
60 | ||
61 | public function doQuery($nameserver, $transport, $queryData, $name) | |
62 | { | |
63 | // we only support UDP right now | |
64 | if ($transport !== 'udp') { | |
65 | return Promise\reject(new \RuntimeException( | |
66 | 'DNS query for ' . $name . ' failed: Requested transport "' . $transport . '" not available, only UDP is supported in this version' | |
67 | )); | |
68 | } | |
69 | ||
70 | $that = $this; | |
71 | $parser = $this->parser; | |
72 | $loop = $this->loop; | |
73 | ||
74 | // UDP connections are instant, so try this without a timer | |
75 | try { | |
76 | $conn = $this->createConnection($nameserver, $transport); | |
77 | } catch (\Exception $e) { | |
78 | return Promise\reject(new \RuntimeException('DNS query for ' . $name . ' failed: ' . $e->getMessage(), 0, $e)); | |
79 | } | |
80 | ||
81 | $deferred = new Deferred(function ($resolve, $reject) use (&$timer, $loop, &$conn, $name) { | |
82 | $reject(new CancellationException(sprintf('DNS query for %s has been cancelled', $name))); | |
83 | ||
84 | if ($timer !== null) { | |
85 | $loop->cancelTimer($timer); | |
86 | } | |
87 | $conn->close(); | |
88 | }); | |
89 | ||
90 | $timer = null; | |
91 | if ($this->timeout !== null) { | |
92 | $timer = $this->loop->addTimer($this->timeout, function () use (&$conn, $name, $deferred) { | |
93 | $conn->close(); | |
94 | $deferred->reject(new TimeoutException(sprintf("DNS query for %s timed out", $name))); | |
95 | }); | |
96 | } | |
97 | ||
98 | $conn->on('data', function ($data) use ($conn, $parser, $deferred, $timer, $loop, $name) { | |
99 | $conn->end(); | |
100 | if ($timer !== null) { | |
101 | $loop->cancelTimer($timer); | |
102 | } | |
103 | ||
104 | try { | |
105 | $response = $parser->parseMessage($data); | |
106 | } catch (\Exception $e) { | |
107 | $deferred->reject($e); | |
108 | return; | |
109 | } | |
110 | ||
111 | if ($response->header->isTruncated()) { | |
112 | $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')); | |
113 | return; | |
114 | } | |
115 | ||
116 | $deferred->resolve($response); | |
117 | }); | |
118 | $conn->write($queryData); | |
119 | ||
120 | return $deferred->promise(); | |
121 | } | |
122 | ||
123 | /** | |
124 | * @deprecated unused, exists for BC only | |
125 | */ | |
126 | protected function generateId() | |
127 | { | |
128 | return mt_rand(0, 0xffff); | |
129 | } | |
130 | ||
131 | /** | |
132 | * @param string $nameserver | |
133 | * @param string $transport | |
134 | * @return \React\Stream\DuplexStreamInterface | |
135 | */ | |
136 | protected function createConnection($nameserver, $transport) | |
137 | { | |
138 | $fd = @stream_socket_client("$transport://$nameserver", $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); | |
139 | if ($fd === false) { | |
140 | throw new \RuntimeException('Unable to connect to DNS server: ' . $errstr, $errno); | |
141 | } | |
142 | ||
143 | // Instantiate stream instance around this stream resource. | |
144 | // This ought to be replaced with a datagram socket in the future. | |
145 | // Temporary work around for Windows 10: buffer whole UDP response | |
146 | // @coverageIgnoreStart | |
147 | if (!class_exists('React\Stream\Stream')) { | |
148 | // prefer DuplexResourceStream as of react/stream v0.7.0 | |
149 | $conn = new DuplexResourceStream($fd, $this->loop, -1); | |
150 | } else { | |
151 | // use legacy Stream class for react/stream < v0.7.0 | |
152 | $conn = new Stream($fd, $this->loop); | |
153 | $conn->bufferSize = null; | |
154 | } | |
155 | // @coverageIgnoreEnd | |
156 | ||
157 | return $conn; | |
158 | } | |
159 | } |
3 | 3 | |
4 | 4 | interface ExecutorInterface |
5 | 5 | { |
6 | public function query($nameserver, Query $query); | |
6 | /** | |
7 | * Executes a query and will return a response message | |
8 | * | |
9 | * It returns a Promise which either fulfills with a response | |
10 | * `React\Dns\Model\Message` on success or rejects with an `Exception` if | |
11 | * the query is not successful. A response message may indicate an error | |
12 | * condition in its `rcode`, but this is considered a valid response message. | |
13 | * | |
14 | * ```php | |
15 | * $executor->query($query)->then( | |
16 | * function (React\Dns\Model\Message $response) { | |
17 | * // response message successfully received | |
18 | * var_dump($response->rcode, $response->answers); | |
19 | * }, | |
20 | * function (Exception $error) { | |
21 | * // failed to query due to $error | |
22 | * } | |
23 | * ); | |
24 | * ``` | |
25 | * | |
26 | * The returned Promise MUST be implemented in such a way that it can be | |
27 | * cancelled when it is still pending. Cancelling a pending promise MUST | |
28 | * reject its value with an Exception. It SHOULD clean up any underlying | |
29 | * resources and references as applicable. | |
30 | * | |
31 | * ```php | |
32 | * $promise = $executor->query($query); | |
33 | * | |
34 | * $promise->cancel(); | |
35 | * ``` | |
36 | * | |
37 | * @param Query $query | |
38 | * @return \React\Promise\PromiseInterface<\React\Dns\Model\Message,\Exception> | |
39 | * resolves with response message on success or rejects with an Exception on error | |
40 | */ | |
41 | public function query(Query $query); | |
7 | 42 | } |
7 | 7 | use React\Promise; |
8 | 8 | |
9 | 9 | /** |
10 | * Resolves hosts from the givne HostsFile or falls back to another executor | |
10 | * Resolves hosts from the given HostsFile or falls back to another executor | |
11 | 11 | * |
12 | 12 | * If the host is found in the hosts file, it will not be passed to the actual |
13 | 13 | * DNS executor. If the host is not found in the hosts file, it will be passed |
14 | 14 | * to the DNS executor as a fallback. |
15 | 15 | */ |
16 | class HostsFileExecutor implements ExecutorInterface | |
16 | final class HostsFileExecutor implements ExecutorInterface | |
17 | 17 | { |
18 | 18 | private $hosts; |
19 | 19 | private $fallback; |
24 | 24 | $this->fallback = $fallback; |
25 | 25 | } |
26 | 26 | |
27 | public function query($nameserver, Query $query) | |
27 | public function query(Query $query) | |
28 | 28 | { |
29 | 29 | if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) { |
30 | 30 | // forward lookup for type A or AAAA |
60 | 60 | } |
61 | 61 | } |
62 | 62 | |
63 | return $this->fallback->query($nameserver, $query); | |
63 | return $this->fallback->query($query); | |
64 | 64 | } |
65 | 65 | |
66 | 66 | private function getIpFromHost($host) |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | class Query | |
4 | /** | |
5 | * This class represents a single question in a query/response message | |
6 | * | |
7 | * It uses a structure similar to `\React\Dns\Message\Record`, but does not | |
8 | * contain fields for resulting TTL and resulting record data (IPs etc.). | |
9 | * | |
10 | * @link https://tools.ietf.org/html/rfc1035#section-4.1.2 | |
11 | * @see \React\Dns\Message\Record | |
12 | */ | |
13 | final class Query | |
5 | 14 | { |
15 | /** | |
16 | * @var string query name, i.e. hostname to look up | |
17 | */ | |
6 | 18 | public $name; |
19 | ||
20 | /** | |
21 | * @var int query type (aka QTYPE), see Message::TYPE_* constants | |
22 | */ | |
7 | 23 | public $type; |
24 | ||
25 | /** | |
26 | * @var int query class (aka QCLASS), see Message::CLASS_IN constant | |
27 | */ | |
8 | 28 | public $class; |
9 | 29 | |
10 | 30 | /** |
11 | * @deprecated still used internally for BC reasons, should not be used externally. | |
31 | * @param string $name query name, i.e. hostname to look up | |
32 | * @param int $type query type, see Message::TYPE_* constants | |
33 | * @param int $class query class, see Message::CLASS_IN constant | |
12 | 34 | */ |
13 | public $currentTime; | |
14 | ||
15 | /** | |
16 | * @param string $name query name, i.e. hostname to look up | |
17 | * @param int $type query type, see Message::TYPE_* constants | |
18 | * @param int $class query class, see Message::CLASS_IN constant | |
19 | * @param int|null $currentTime (deprecated) still used internally, should not be passed explicitly anymore. | |
20 | */ | |
21 | public function __construct($name, $type, $class, $currentTime = null) | |
35 | public function __construct($name, $type, $class) | |
22 | 36 | { |
23 | if($currentTime === null) { | |
24 | $currentTime = time(); | |
25 | } | |
26 | ||
27 | 37 | $this->name = $name; |
28 | 38 | $this->type = $type; |
29 | 39 | $this->class = $class; |
30 | $this->currentTime = $currentTime; | |
31 | 40 | } |
32 | 41 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Record; | |
5 | ||
6 | class RecordBag | |
7 | { | |
8 | private $records = array(); | |
9 | ||
10 | public function set($currentTime, Record $record) | |
11 | { | |
12 | $this->records[] = array($currentTime + $record->ttl, $record); | |
13 | } | |
14 | ||
15 | public function all() | |
16 | { | |
17 | return array_values(array_map( | |
18 | function ($value) { | |
19 | list($expiresAt, $record) = $value; | |
20 | return $record; | |
21 | }, | |
22 | $this->records | |
23 | )); | |
24 | } | |
25 | } |
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 | use React\Promise\PromiseInterface; | |
9 | ||
10 | /** | |
11 | * Wraps an underlying cache interface and exposes only cached DNS data | |
12 | */ | |
13 | class RecordCache | |
14 | { | |
15 | private $cache; | |
16 | private $expiredAt; | |
17 | ||
18 | public function __construct(CacheInterface $cache) | |
19 | { | |
20 | $this->cache = $cache; | |
21 | } | |
22 | ||
23 | /** | |
24 | * Looks up the cache if there's a cached answer for the given query | |
25 | * | |
26 | * @param Query $query | |
27 | * @return PromiseInterface Promise<Record[],mixed> resolves with array of Record objects on sucess | |
28 | * or rejects with mixed values when query is not cached already. | |
29 | */ | |
30 | public function lookup(Query $query) | |
31 | { | |
32 | $id = $this->serializeQueryToIdentity($query); | |
33 | ||
34 | $expiredAt = $this->expiredAt; | |
35 | ||
36 | return $this->cache | |
37 | ->get($id) | |
38 | ->then(function ($value) use ($query, $expiredAt) { | |
39 | // cache 0.5+ resolves with null on cache miss, return explicit cache miss here | |
40 | if ($value === null) { | |
41 | return Promise\reject(); | |
42 | } | |
43 | ||
44 | /* @var $recordBag RecordBag */ | |
45 | $recordBag = unserialize($value); | |
46 | ||
47 | // reject this cache hit if the query was started before the time we expired the cache? | |
48 | // todo: this is a legacy left over, this value is never actually set, so this never applies. | |
49 | // todo: this should probably validate the cache time instead. | |
50 | if (null !== $expiredAt && $expiredAt <= $query->currentTime) { | |
51 | return Promise\reject(); | |
52 | } | |
53 | ||
54 | return $recordBag->all(); | |
55 | }); | |
56 | } | |
57 | ||
58 | /** | |
59 | * Stores all records from this response message in the cache | |
60 | * | |
61 | * @param int $currentTime | |
62 | * @param Message $message | |
63 | * @uses self::storeRecord() | |
64 | */ | |
65 | public function storeResponseMessage($currentTime, Message $message) | |
66 | { | |
67 | foreach ($message->answers as $record) { | |
68 | $this->storeRecord($currentTime, $record); | |
69 | } | |
70 | } | |
71 | ||
72 | /** | |
73 | * Stores a single record from a response message in the cache | |
74 | * | |
75 | * @param int $currentTime | |
76 | * @param Record $record | |
77 | */ | |
78 | public function storeRecord($currentTime, Record $record) | |
79 | { | |
80 | $id = $this->serializeRecordToIdentity($record); | |
81 | ||
82 | $cache = $this->cache; | |
83 | ||
84 | $this->cache | |
85 | ->get($id) | |
86 | ->then( | |
87 | function ($value) { | |
88 | if ($value === null) { | |
89 | // cache 0.5+ cache miss resolves with null, return empty bag here | |
90 | return new RecordBag(); | |
91 | } | |
92 | ||
93 | // reuse existing bag on cache hit to append new record to it | |
94 | return unserialize($value); | |
95 | }, | |
96 | function ($e) { | |
97 | // legacy cache < 0.5 cache miss rejects promise, return empty bag here | |
98 | return new RecordBag(); | |
99 | } | |
100 | ) | |
101 | ->then(function (RecordBag $recordBag) use ($id, $currentTime, $record, $cache) { | |
102 | // add a record to the existing (possibly empty) record bag and save to cache | |
103 | $recordBag->set($currentTime, $record); | |
104 | $cache->set($id, serialize($recordBag)); | |
105 | }); | |
106 | } | |
107 | ||
108 | public function expire($currentTime) | |
109 | { | |
110 | $this->expiredAt = $currentTime; | |
111 | } | |
112 | ||
113 | public function serializeQueryToIdentity(Query $query) | |
114 | { | |
115 | return sprintf('%s:%s:%s', $query->name, $query->type, $query->class); | |
116 | } | |
117 | ||
118 | public function serializeRecordToIdentity(Record $record) | |
119 | { | |
120 | return sprintf('%s:%s:%s', $record->name, $record->type, $record->class); | |
121 | } | |
122 | } |
4 | 4 | use React\Promise\CancellablePromiseInterface; |
5 | 5 | use React\Promise\Deferred; |
6 | 6 | |
7 | class RetryExecutor implements ExecutorInterface | |
7 | final class RetryExecutor implements ExecutorInterface | |
8 | 8 | { |
9 | 9 | private $executor; |
10 | 10 | private $retries; |
15 | 15 | $this->retries = $retries; |
16 | 16 | } |
17 | 17 | |
18 | public function query($nameserver, Query $query) | |
18 | public function query(Query $query) | |
19 | 19 | { |
20 | return $this->tryQuery($nameserver, $query, $this->retries); | |
20 | return $this->tryQuery($query, $this->retries); | |
21 | 21 | } |
22 | 22 | |
23 | public function tryQuery($nameserver, Query $query, $retries) | |
23 | public function tryQuery(Query $query, $retries) | |
24 | 24 | { |
25 | 25 | $deferred = new Deferred(function () use (&$promise) { |
26 | 26 | if ($promise instanceof CancellablePromiseInterface) { |
34 | 34 | }; |
35 | 35 | |
36 | 36 | $executor = $this->executor; |
37 | $errorback = function ($e) use ($deferred, &$promise, $nameserver, $query, $success, &$errorback, &$retries, $executor) { | |
37 | $errorback = function ($e) use ($deferred, &$promise, $query, $success, &$errorback, &$retries, $executor) { | |
38 | 38 | if (!$e instanceof TimeoutException) { |
39 | 39 | $errorback = null; |
40 | 40 | $deferred->reject($e); |
61 | 61 | $r->setValue($e, $trace); |
62 | 62 | } else { |
63 | 63 | --$retries; |
64 | $promise = $executor->query($nameserver, $query)->then( | |
64 | $promise = $executor->query($query)->then( | |
65 | 65 | $success, |
66 | 66 | $errorback |
67 | 67 | ); |
68 | 68 | } |
69 | 69 | }; |
70 | 70 | |
71 | $promise = $this->executor->query($nameserver, $query)->then( | |
71 | $promise = $this->executor->query($query)->then( | |
72 | 72 | $success, |
73 | 73 | $errorback |
74 | 74 | ); |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Promise\Promise; | |
5 | ||
6 | /** | |
7 | * Send DNS queries over a UDP or TCP/IP stream transport. | |
8 | * | |
9 | * This class will automatically choose the correct transport protocol to send | |
10 | * a DNS query to your DNS server. It will always try to send it over the more | |
11 | * efficient UDP transport first. If this query yields a size related issue | |
12 | * (truncated messages), it will retry over a streaming TCP/IP transport. | |
13 | * | |
14 | * For more advanced usages one can utilize this class directly. | |
15 | * The following example looks up the `IPv6` address for `reactphp.org`. | |
16 | * | |
17 | * ```php | |
18 | * $executor = new SelectiveTransportExecutor($udpExecutor, $tcpExecutor); | |
19 | * | |
20 | * $executor->query( | |
21 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
22 | * )->then(function (Message $message) { | |
23 | * foreach ($message->answers as $answer) { | |
24 | * echo 'IPv6: ' . $answer->data . PHP_EOL; | |
25 | * } | |
26 | * }, 'printf'); | |
27 | * ``` | |
28 | * | |
29 | * Note that this executor only implements the logic to select the correct | |
30 | * transport for the given DNS query. Implementing the correct transport logic, | |
31 | * implementing timeouts and any retry logic is left up to the given executors, | |
32 | * see also [`UdpTransportExecutor`](#udptransportexecutor) and | |
33 | * [`TcpTransportExecutor`](#tcptransportexecutor) for more details. | |
34 | * | |
35 | * Note that this executor is entirely async and as such allows you to execute | |
36 | * any number of queries concurrently. You should probably limit the number of | |
37 | * concurrent queries in your application or you're very likely going to face | |
38 | * rate limitations and bans on the resolver end. For many common applications, | |
39 | * you may want to avoid sending the same query multiple times when the first | |
40 | * one is still pending, so you will likely want to use this in combination with | |
41 | * a `CoopExecutor` like this: | |
42 | * | |
43 | * ```php | |
44 | * $executor = new CoopExecutor( | |
45 | * new SelectiveTransportExecutor( | |
46 | * $datagramExecutor, | |
47 | * $streamExecutor | |
48 | * ) | |
49 | * ); | |
50 | * ``` | |
51 | */ | |
52 | class SelectiveTransportExecutor implements ExecutorInterface | |
53 | { | |
54 | private $datagramExecutor; | |
55 | private $streamExecutor; | |
56 | ||
57 | public function __construct(ExecutorInterface $datagramExecutor, ExecutorInterface $streamExecutor) | |
58 | { | |
59 | $this->datagramExecutor = $datagramExecutor; | |
60 | $this->streamExecutor = $streamExecutor; | |
61 | } | |
62 | ||
63 | public function query(Query $query) | |
64 | { | |
65 | $stream = $this->streamExecutor; | |
66 | $pending = $this->datagramExecutor->query($query); | |
67 | ||
68 | return new Promise(function ($resolve, $reject) use (&$pending, $stream, $query) { | |
69 | $pending->then( | |
70 | $resolve, | |
71 | function ($e) use (&$pending, $stream, $query, $resolve, $reject) { | |
72 | if ($e->getCode() === (\defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90)) { | |
73 | $pending = $stream->query($query)->then($resolve, $reject); | |
74 | } else { | |
75 | $reject($e); | |
76 | } | |
77 | } | |
78 | ); | |
79 | }, function () use (&$pending) { | |
80 | $pending->cancel(); | |
81 | $pending = null; | |
82 | }); | |
83 | } | |
84 | } |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Protocol\BinaryDumper; | |
6 | use React\Dns\Protocol\Parser; | |
7 | use React\EventLoop\LoopInterface; | |
8 | use React\Promise\Deferred; | |
9 | ||
10 | /** | |
11 | * Send DNS queries over a TCP/IP stream transport. | |
12 | * | |
13 | * This is one of the main classes that send a DNS query to your DNS server. | |
14 | * | |
15 | * For more advanced usages one can utilize this class directly. | |
16 | * The following example looks up the `IPv6` address for `reactphp.org`. | |
17 | * | |
18 | * ```php | |
19 | * $loop = Factory::create(); | |
20 | * $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
21 | * | |
22 | * $executor->query( | |
23 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) | |
24 | * )->then(function (Message $message) { | |
25 | * foreach ($message->answers as $answer) { | |
26 | * echo 'IPv6: ' . $answer->data . PHP_EOL; | |
27 | * } | |
28 | * }, 'printf'); | |
29 | * | |
30 | * $loop->run(); | |
31 | * ``` | |
32 | * | |
33 | * See also [example #92](examples). | |
34 | * | |
35 | * Note that this executor does not implement a timeout, so you will very likely | |
36 | * want to use this in combination with a `TimeoutExecutor` like this: | |
37 | * | |
38 | * ```php | |
39 | * $executor = new TimeoutExecutor( | |
40 | * new TcpTransportExecutor($nameserver, $loop), | |
41 | * 3.0, | |
42 | * $loop | |
43 | * ); | |
44 | * ``` | |
45 | * | |
46 | * Unlike the `UdpTransportExecutor`, this class uses a reliable TCP/IP | |
47 | * transport, so you do not necessarily have to implement any retry logic. | |
48 | * | |
49 | * Note that this executor is entirely async and as such allows you to execute | |
50 | * queries concurrently. The first query will establish a TCP/IP socket | |
51 | * connection to the DNS server which will be kept open for a short period. | |
52 | * Additional queries will automatically reuse this existing socket connection | |
53 | * to the DNS server, will pipeline multiple requests over this single | |
54 | * connection and will keep an idle connection open for a short period. The | |
55 | * initial TCP/IP connection overhead may incur a slight delay if you only send | |
56 | * occasional queries – when sending a larger number of concurrent queries over | |
57 | * an existing connection, it becomes increasingly more efficient and avoids | |
58 | * creating many concurrent sockets like the UDP-based executor. You may still | |
59 | * want to limit the number of (concurrent) queries in your application or you | |
60 | * may be facing rate limitations and bans on the resolver end. For many common | |
61 | * applications, you may want to avoid sending the same query multiple times | |
62 | * when the first one is still pending, so you will likely want to use this in | |
63 | * combination with a `CoopExecutor` like this: | |
64 | * | |
65 | * ```php | |
66 | * $executor = new CoopExecutor( | |
67 | * new TimeoutExecutor( | |
68 | * new TcpTransportExecutor($nameserver, $loop), | |
69 | * 3.0, | |
70 | * $loop | |
71 | * ) | |
72 | * ); | |
73 | * ``` | |
74 | * | |
75 | * > Internally, this class uses PHP's TCP/IP sockets and does not take advantage | |
76 | * of [react/socket](https://github.com/reactphp/socket) purely for | |
77 | * organizational reasons to avoid a cyclic dependency between the two | |
78 | * packages. Higher-level components should take advantage of the Socket | |
79 | * component instead of reimplementing this socket logic from scratch. | |
80 | */ | |
81 | class TcpTransportExecutor implements ExecutorInterface | |
82 | { | |
83 | private $nameserver; | |
84 | private $loop; | |
85 | private $parser; | |
86 | private $dumper; | |
87 | ||
88 | /** | |
89 | * @var ?resource | |
90 | */ | |
91 | private $socket; | |
92 | ||
93 | /** | |
94 | * @var Deferred[] | |
95 | */ | |
96 | private $pending = array(); | |
97 | ||
98 | /** | |
99 | * @var string[] | |
100 | */ | |
101 | private $names = array(); | |
102 | ||
103 | /** | |
104 | * Maximum idle time when socket is current unused (i.e. no pending queries outstanding) | |
105 | * | |
106 | * If a new query is to be sent during the idle period, we can reuse the | |
107 | * existing socket without having to wait for a new socket connection. | |
108 | * This uses a rather small, hard-coded value to not keep any unneeded | |
109 | * sockets open and to not keep the loop busy longer than needed. | |
110 | * | |
111 | * A future implementation may take advantage of `edns-tcp-keepalive` to keep | |
112 | * the socket open for longer periods. This will likely require explicit | |
113 | * configuration because this may consume additional resources and also keep | |
114 | * the loop busy for longer than expected in some applications. | |
115 | * | |
116 | * @var float | |
117 | * @link https://tools.ietf.org/html/rfc7766#section-6.2.1 | |
118 | * @link https://tools.ietf.org/html/rfc7828 | |
119 | */ | |
120 | private $idlePeriod = 0.001; | |
121 | ||
122 | /** | |
123 | * @var ?\React\EventLoop\TimerInterface | |
124 | */ | |
125 | private $idleTimer; | |
126 | ||
127 | private $writeBuffer = ''; | |
128 | private $writePending = false; | |
129 | ||
130 | private $readBuffer = ''; | |
131 | private $readPending = false; | |
132 | ||
133 | /** | |
134 | * @param string $nameserver | |
135 | * @param LoopInterface $loop | |
136 | */ | |
137 | public function __construct($nameserver, LoopInterface $loop) | |
138 | { | |
139 | if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) { | |
140 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
141 | $nameserver = '[' . $nameserver . ']'; | |
142 | } | |
143 | ||
144 | $parts = \parse_url((\strpos($nameserver, '://') === false ? 'tcp://' : '') . $nameserver); | |
145 | if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'tcp' || !\filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { | |
146 | throw new \InvalidArgumentException('Invalid nameserver address given'); | |
147 | } | |
148 | ||
149 | $this->nameserver = $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); | |
150 | $this->loop = $loop; | |
151 | $this->parser = new Parser(); | |
152 | $this->dumper = new BinaryDumper(); | |
153 | } | |
154 | ||
155 | public function query(Query $query) | |
156 | { | |
157 | $request = Message::createRequestForQuery($query); | |
158 | ||
159 | // keep shuffing message ID to avoid using the same message ID for two pending queries at the same time | |
160 | while (isset($this->pending[$request->id])) { | |
161 | $request->id = \mt_rand(0, 0xffff); // @codeCoverageIgnore | |
162 | } | |
163 | ||
164 | $queryData = $this->dumper->toBinary($request); | |
165 | $length = \strlen($queryData); | |
166 | if ($length > 0xffff) { | |
167 | return \React\Promise\reject(new \RuntimeException( | |
168 | 'DNS query for ' . $query->name . ' failed: Query too large for TCP transport' | |
169 | )); | |
170 | } | |
171 | ||
172 | $queryData = \pack('n', $length) . $queryData; | |
173 | ||
174 | if ($this->socket === null) { | |
175 | // create async TCP/IP connection (may take a while) | |
176 | $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT); | |
177 | if ($socket === false) { | |
178 | return \React\Promise\reject(new \RuntimeException( | |
179 | 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', | |
180 | $errno | |
181 | )); | |
182 | } | |
183 | ||
184 | // set socket to non-blocking and wait for it to become writable (connection success/rejected) | |
185 | \stream_set_blocking($socket, false); | |
186 | $this->socket = $socket; | |
187 | } | |
188 | ||
189 | if ($this->idleTimer !== null) { | |
190 | $this->loop->cancelTimer($this->idleTimer); | |
191 | $this->idleTimer = null; | |
192 | } | |
193 | ||
194 | // wait for socket to become writable to actually write out data | |
195 | $this->writeBuffer .= $queryData; | |
196 | if (!$this->writePending) { | |
197 | $this->writePending = true; | |
198 | $this->loop->addWriteStream($this->socket, array($this, 'handleWritable')); | |
199 | } | |
200 | ||
201 | $names =& $this->names; | |
202 | $that = $this; | |
203 | $deferred = new Deferred(function () use ($that, &$names, $request) { | |
204 | // remove from list of pending names, but remember pending query | |
205 | $name = $names[$request->id]; | |
206 | unset($names[$request->id]); | |
207 | $that->checkIdle(); | |
208 | ||
209 | throw new CancellationException('DNS query for ' . $name . ' has been cancelled'); | |
210 | }); | |
211 | ||
212 | $this->pending[$request->id] = $deferred; | |
213 | $this->names[$request->id] = $query->name; | |
214 | ||
215 | return $deferred->promise(); | |
216 | } | |
217 | ||
218 | /** | |
219 | * @internal | |
220 | */ | |
221 | public function handleWritable() | |
222 | { | |
223 | if ($this->readPending === false) { | |
224 | $name = @\stream_socket_get_name($this->socket, true); | |
225 | if ($name === false) { | |
226 | $this->closeError('Connection to DNS server rejected'); | |
227 | return; | |
228 | } | |
229 | ||
230 | $this->readPending = true; | |
231 | $this->loop->addReadStream($this->socket, array($this, 'handleRead')); | |
232 | } | |
233 | ||
234 | $written = @\fwrite($this->socket, $this->writeBuffer); | |
235 | if ($written === false || $written === 0) { | |
236 | $this->closeError('Unable to write to closed socket'); | |
237 | return; | |
238 | } | |
239 | ||
240 | if (isset($this->writeBuffer[$written])) { | |
241 | $this->writeBuffer = \substr($this->writeBuffer, $written); | |
242 | } else { | |
243 | $this->loop->removeWriteStream($this->socket); | |
244 | $this->writePending = false; | |
245 | $this->writeBuffer = ''; | |
246 | } | |
247 | } | |
248 | ||
249 | /** | |
250 | * @internal | |
251 | */ | |
252 | public function handleRead() | |
253 | { | |
254 | // read one chunk of data from the DNS server | |
255 | // any error is fatal, this is a stream of TCP/IP data | |
256 | $chunk = @\fread($this->socket, 65536); | |
257 | if ($chunk === false || $chunk === '') { | |
258 | $this->closeError('Connection to DNS server lost'); | |
259 | return; | |
260 | } | |
261 | ||
262 | // reassemble complete message by concatenating all chunks. | |
263 | $this->readBuffer .= $chunk; | |
264 | ||
265 | // response message header contains at least 12 bytes | |
266 | while (isset($this->readBuffer[11])) { | |
267 | // read response message length from first 2 bytes and ensure we have length + data in buffer | |
268 | list(, $length) = \unpack('n', $this->readBuffer); | |
269 | if (!isset($this->readBuffer[$length + 1])) { | |
270 | return; | |
271 | } | |
272 | ||
273 | $data = \substr($this->readBuffer, 2, $length); | |
274 | $this->readBuffer = (string)substr($this->readBuffer, $length + 2); | |
275 | ||
276 | try { | |
277 | $response = $this->parser->parseMessage($data); | |
278 | } catch (\Exception $e) { | |
279 | // reject all pending queries if we received an invalid message from remote server | |
280 | $this->closeError('Invalid message received from DNS server'); | |
281 | return; | |
282 | } | |
283 | ||
284 | // reject all pending queries if we received an unexpected response ID or truncated response | |
285 | if (!isset($this->pending[$response->id]) || $response->tc) { | |
286 | $this->closeError('Invalid response message received from DNS server'); | |
287 | return; | |
288 | } | |
289 | ||
290 | $deferred = $this->pending[$response->id]; | |
291 | unset($this->pending[$response->id], $this->names[$response->id]); | |
292 | ||
293 | $deferred->resolve($response); | |
294 | ||
295 | $this->checkIdle(); | |
296 | } | |
297 | } | |
298 | ||
299 | /** | |
300 | * @internal | |
301 | * @param string $reason | |
302 | */ | |
303 | public function closeError($reason) | |
304 | { | |
305 | $this->readBuffer = ''; | |
306 | if ($this->readPending) { | |
307 | $this->loop->removeReadStream($this->socket); | |
308 | $this->readPending = false; | |
309 | } | |
310 | ||
311 | $this->writeBuffer = ''; | |
312 | if ($this->writePending) { | |
313 | $this->loop->removeWriteStream($this->socket); | |
314 | $this->writePending = false; | |
315 | } | |
316 | ||
317 | if ($this->idleTimer !== null) { | |
318 | $this->loop->cancelTimer($this->idleTimer); | |
319 | $this->idleTimer = null; | |
320 | } | |
321 | ||
322 | @\fclose($this->socket); | |
323 | $this->socket = null; | |
324 | ||
325 | foreach ($this->names as $id => $name) { | |
326 | $this->pending[$id]->reject(new \RuntimeException( | |
327 | 'DNS query for ' . $name . ' failed: ' . $reason | |
328 | )); | |
329 | } | |
330 | $this->pending = $this->names = array(); | |
331 | } | |
332 | ||
333 | /** | |
334 | * @internal | |
335 | */ | |
336 | public function checkIdle() | |
337 | { | |
338 | if ($this->idleTimer === null && !$this->names) { | |
339 | $that = $this; | |
340 | $this->idleTimer = $this->loop->addTimer($this->idlePeriod, function () use ($that) { | |
341 | $that->closeError('Idle timeout'); | |
342 | }); | |
343 | } | |
344 | } | |
345 | } |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | class TimeoutException extends \Exception | |
4 | final class TimeoutException extends \Exception | |
5 | 5 | { |
6 | 6 | } |
6 | 6 | use React\Promise\CancellablePromiseInterface; |
7 | 7 | use React\Promise\Timer; |
8 | 8 | |
9 | class TimeoutExecutor implements ExecutorInterface | |
9 | final class TimeoutExecutor implements ExecutorInterface | |
10 | 10 | { |
11 | 11 | private $executor; |
12 | 12 | private $loop; |
19 | 19 | $this->timeout = $timeout; |
20 | 20 | } |
21 | 21 | |
22 | public function query($nameserver, Query $query) | |
22 | public function query(Query $query) | |
23 | 23 | { |
24 | return Timer\timeout($this->executor->query($nameserver, $query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { | |
24 | return Timer\timeout($this->executor->query($query), $this->timeout, $this->loop)->then(null, function ($e) use ($query) { | |
25 | 25 | if ($e instanceof Timer\TimeoutException) { |
26 | 26 | $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e); |
27 | 27 | } |
18 | 18 | * |
19 | 19 | * ```php |
20 | 20 | * $loop = Factory::create(); |
21 | * $executor = new UdpTransportExecutor($loop); | |
21 | * $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
22 | 22 | * |
23 | 23 | * $executor->query( |
24 | * '8.8.8.8:53', | |
25 | 24 | * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN) |
26 | 25 | * )->then(function (Message $message) { |
27 | 26 | * foreach ($message->answers as $answer) { |
39 | 38 | * |
40 | 39 | * ```php |
41 | 40 | * $executor = new TimeoutExecutor( |
42 | * new UdpTransportExecutor($loop), | |
41 | * new UdpTransportExecutor($nameserver, $loop), | |
43 | 42 | * 3.0, |
44 | 43 | * $loop |
45 | 44 | * ); |
52 | 51 | * ```php |
53 | 52 | * $executor = new RetryExecutor( |
54 | 53 | * new TimeoutExecutor( |
55 | * new UdpTransportExecutor($loop), | |
54 | * new UdpTransportExecutor($nameserver, $loop), | |
56 | 55 | * 3.0, |
57 | 56 | * $loop |
57 | * ) | |
58 | * ); | |
59 | * ``` | |
60 | * | |
61 | * Note that this executor is entirely async and as such allows you to execute | |
62 | * any number of queries concurrently. You should probably limit the number of | |
63 | * concurrent queries in your application or you're very likely going to face | |
64 | * rate limitations and bans on the resolver end. For many common applications, | |
65 | * you may want to avoid sending the same query multiple times when the first | |
66 | * one is still pending, so you will likely want to use this in combination with | |
67 | * a `CoopExecutor` like this: | |
68 | * | |
69 | * ```php | |
70 | * $executor = new CoopExecutor( | |
71 | * new RetryExecutor( | |
72 | * new TimeoutExecutor( | |
73 | * new UdpTransportExecutor($nameserver, $loop), | |
74 | * 3.0, | |
75 | * $loop | |
76 | * ) | |
58 | 77 | * ) |
59 | 78 | * ); |
60 | 79 | * ``` |
65 | 84 | * packages. Higher-level components should take advantage of the Datagram |
66 | 85 | * component instead of reimplementing this socket logic from scratch. |
67 | 86 | */ |
68 | class UdpTransportExecutor implements ExecutorInterface | |
87 | final class UdpTransportExecutor implements ExecutorInterface | |
69 | 88 | { |
89 | private $nameserver; | |
70 | 90 | private $loop; |
71 | 91 | private $parser; |
72 | 92 | private $dumper; |
73 | 93 | |
74 | 94 | /** |
75 | * @param LoopInterface $loop | |
76 | * @param null|Parser $parser optional/advanced: DNS protocol parser to use | |
77 | * @param null|BinaryDumper $dumper optional/advanced: DNS protocol dumper to use | |
95 | * @param string $nameserver | |
96 | * @param LoopInterface $loop | |
78 | 97 | */ |
79 | public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null) | |
98 | public function __construct($nameserver, LoopInterface $loop) | |
80 | 99 | { |
81 | if ($parser === null) { | |
82 | $parser = new Parser(); | |
83 | } | |
84 | if ($dumper === null) { | |
85 | $dumper = new BinaryDumper(); | |
100 | if (\strpos($nameserver, '[') === false && \substr_count($nameserver, ':') >= 2 && \strpos($nameserver, '://') === false) { | |
101 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
102 | $nameserver = '[' . $nameserver . ']'; | |
86 | 103 | } |
87 | 104 | |
105 | $parts = \parse_url((\strpos($nameserver, '://') === false ? 'udp://' : '') . $nameserver); | |
106 | if (!isset($parts['scheme'], $parts['host']) || $parts['scheme'] !== 'udp' || !\filter_var(\trim($parts['host'], '[]'), \FILTER_VALIDATE_IP)) { | |
107 | throw new \InvalidArgumentException('Invalid nameserver address given'); | |
108 | } | |
109 | ||
110 | $this->nameserver = 'udp://' . $parts['host'] . ':' . (isset($parts['port']) ? $parts['port'] : 53); | |
88 | 111 | $this->loop = $loop; |
89 | $this->parser = $parser; | |
90 | $this->dumper = $dumper; | |
112 | $this->parser = new Parser(); | |
113 | $this->dumper = new BinaryDumper(); | |
91 | 114 | } |
92 | 115 | |
93 | public function query($nameserver, Query $query) | |
116 | public function query(Query $query) | |
94 | 117 | { |
95 | 118 | $request = Message::createRequestForQuery($query); |
96 | 119 | |
97 | 120 | $queryData = $this->dumper->toBinary($request); |
98 | 121 | if (isset($queryData[512])) { |
99 | 122 | return \React\Promise\reject(new \RuntimeException( |
100 | 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport' | |
123 | 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport', | |
124 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 | |
101 | 125 | )); |
102 | 126 | } |
103 | 127 | |
104 | 128 | // UDP connections are instant, so try connection without a loop or timeout |
105 | $socket = @\stream_socket_client("udp://$nameserver", $errno, $errstr, 0); | |
129 | $socket = @\stream_socket_client($this->nameserver, $errno, $errstr, 0); | |
106 | 130 | if ($socket === false) { |
107 | 131 | return \React\Promise\reject(new \RuntimeException( |
108 | 132 | 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')', |
139 | 163 | |
140 | 164 | // ignore and await next if we received an unexpected response ID |
141 | 165 | // this may as well be a fake response from an attacker (possible cache poisoning) |
142 | if ($response->getId() !== $request->getId()) { | |
166 | if ($response->id !== $request->id) { | |
143 | 167 | return; |
144 | 168 | } |
145 | 169 | |
147 | 171 | $loop->removeReadStream($socket); |
148 | 172 | \fclose($socket); |
149 | 173 | |
150 | if ($response->header->isTruncated()) { | |
151 | $deferred->reject(new \RuntimeException('DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query, but retrying via TCP is currently not supported')); | |
174 | if ($response->tc) { | |
175 | $deferred->reject(new \RuntimeException( | |
176 | 'DNS query for ' . $query->name . ' failed: The server returned a truncated result for a UDP query', | |
177 | \defined('SOCKET_EMSGSIZE') ? \SOCKET_EMSGSIZE : 90 | |
178 | )); | |
152 | 179 | return; |
153 | 180 | } |
154 | 181 |
1 | 1 | |
2 | 2 | namespace React\Dns; |
3 | 3 | |
4 | class RecordNotFoundException extends \Exception | |
4 | final class RecordNotFoundException extends \Exception | |
5 | 5 | { |
6 | 6 | } |
4 | 4 | use React\Cache\ArrayCache; |
5 | 5 | use React\Cache\CacheInterface; |
6 | 6 | use React\Dns\Config\HostsFile; |
7 | use React\Dns\Query\CachedExecutor; | |
7 | use React\Dns\Query\CachingExecutor; | |
8 | use React\Dns\Query\CoopExecutor; | |
8 | 9 | use React\Dns\Query\ExecutorInterface; |
9 | 10 | use React\Dns\Query\HostsFileExecutor; |
10 | use React\Dns\Query\RecordCache; | |
11 | 11 | use React\Dns\Query\RetryExecutor; |
12 | use React\Dns\Query\SelectiveTransportExecutor; | |
13 | use React\Dns\Query\TcpTransportExecutor; | |
12 | 14 | use React\Dns\Query\TimeoutExecutor; |
13 | 15 | use React\Dns\Query\UdpTransportExecutor; |
14 | 16 | use React\EventLoop\LoopInterface; |
15 | 17 | |
16 | class Factory | |
18 | final class Factory | |
17 | 19 | { |
20 | /** | |
21 | * @param string $nameserver | |
22 | * @param LoopInterface $loop | |
23 | * @return \React\Dns\Resolver\ResolverInterface | |
24 | */ | |
18 | 25 | public function create($nameserver, LoopInterface $loop) |
19 | 26 | { |
20 | $nameserver = $this->addPortToServerIfMissing($nameserver); | |
21 | $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop)); | |
27 | $executor = $this->decorateHostsFileExecutor($this->createExecutor($nameserver, $loop)); | |
22 | 28 | |
23 | return new Resolver($nameserver, $executor); | |
29 | return new Resolver($executor); | |
24 | 30 | } |
25 | 31 | |
32 | /** | |
33 | * @param string $nameserver | |
34 | * @param LoopInterface $loop | |
35 | * @param ?CacheInterface $cache | |
36 | * @return \React\Dns\Resolver\ResolverInterface | |
37 | */ | |
26 | 38 | public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null) |
27 | 39 | { |
40 | // default to keeping maximum of 256 responses in cache unless explicitly given | |
28 | 41 | if (!($cache instanceof CacheInterface)) { |
29 | $cache = new ArrayCache(); | |
42 | $cache = new ArrayCache(256); | |
30 | 43 | } |
31 | 44 | |
32 | $nameserver = $this->addPortToServerIfMissing($nameserver); | |
33 | $executor = $this->decorateHostsFileExecutor($this->createCachedExecutor($loop, $cache)); | |
45 | $executor = $this->createExecutor($nameserver, $loop); | |
46 | $executor = new CachingExecutor($executor, $cache); | |
47 | $executor = $this->decorateHostsFileExecutor($executor); | |
34 | 48 | |
35 | return new Resolver($nameserver, $executor); | |
49 | return new Resolver($executor); | |
36 | 50 | } |
37 | 51 | |
38 | 52 | /** |
65 | 79 | return $executor; |
66 | 80 | } |
67 | 81 | |
68 | protected function createExecutor(LoopInterface $loop) | |
82 | private function createExecutor($nameserver, LoopInterface $loop) | |
83 | { | |
84 | $parts = \parse_url($nameserver); | |
85 | ||
86 | if (isset($parts['scheme']) && $parts['scheme'] === 'tcp') { | |
87 | $executor = $this->createTcpExecutor($nameserver, $loop); | |
88 | } elseif (isset($parts['scheme']) && $parts['scheme'] === 'udp') { | |
89 | $executor = $this->createUdpExecutor($nameserver, $loop); | |
90 | } else { | |
91 | $executor = new SelectiveTransportExecutor( | |
92 | $this->createUdpExecutor($nameserver, $loop), | |
93 | $this->createTcpExecutor($nameserver, $loop) | |
94 | ); | |
95 | } | |
96 | ||
97 | return new CoopExecutor($executor); | |
98 | } | |
99 | ||
100 | private function createTcpExecutor($nameserver, LoopInterface $loop) | |
69 | 101 | { |
70 | 102 | return new TimeoutExecutor( |
71 | new UdpTransportExecutor($loop), | |
103 | new TcpTransportExecutor($nameserver, $loop), | |
72 | 104 | 5.0, |
73 | 105 | $loop |
74 | 106 | ); |
75 | 107 | } |
76 | 108 | |
77 | protected function createRetryExecutor(LoopInterface $loop) | |
109 | private function createUdpExecutor($nameserver, LoopInterface $loop) | |
78 | 110 | { |
79 | return new RetryExecutor($this->createExecutor($loop)); | |
80 | } | |
81 | ||
82 | protected function createCachedExecutor(LoopInterface $loop, CacheInterface $cache) | |
83 | { | |
84 | return new CachedExecutor($this->createRetryExecutor($loop), new RecordCache($cache)); | |
85 | } | |
86 | ||
87 | protected function addPortToServerIfMissing($nameserver) | |
88 | { | |
89 | if (strpos($nameserver, '[') === false && substr_count($nameserver, ':') >= 2) { | |
90 | // several colons, but not enclosed in square brackets => enclose IPv6 address in square brackets | |
91 | $nameserver = '[' . $nameserver . ']'; | |
92 | } | |
93 | // assume a dummy scheme when checking for the port, otherwise parse_url() fails | |
94 | if (parse_url('dummy://' . $nameserver, PHP_URL_PORT) === null) { | |
95 | $nameserver .= ':53'; | |
96 | } | |
97 | ||
98 | return $nameserver; | |
111 | return new RetryExecutor( | |
112 | new TimeoutExecutor( | |
113 | new UdpTransportExecutor( | |
114 | $nameserver, | |
115 | $loop | |
116 | ), | |
117 | 5.0, | |
118 | $loop | |
119 | ) | |
120 | ); | |
99 | 121 | } |
100 | 122 | } |
5 | 5 | use React\Dns\Query\ExecutorInterface; |
6 | 6 | use React\Dns\Query\Query; |
7 | 7 | use React\Dns\RecordNotFoundException; |
8 | use React\Promise\PromiseInterface; | |
9 | 8 | |
10 | class Resolver | |
9 | /** | |
10 | * @see ResolverInterface for the base interface | |
11 | */ | |
12 | final class Resolver implements ResolverInterface | |
11 | 13 | { |
12 | private $nameserver; | |
13 | 14 | private $executor; |
14 | 15 | |
15 | public function __construct($nameserver, ExecutorInterface $executor) | |
16 | public function __construct(ExecutorInterface $executor) | |
16 | 17 | { |
17 | $this->nameserver = $nameserver; | |
18 | 18 | $this->executor = $executor; |
19 | 19 | } |
20 | 20 | |
21 | /** | |
22 | * Resolves the given $domain name to a single IPv4 address (type `A` query). | |
23 | * | |
24 | * ```php | |
25 | * $resolver->resolve('reactphp.org')->then(function ($ip) { | |
26 | * echo 'IP for reactphp.org is ' . $ip . PHP_EOL; | |
27 | * }); | |
28 | * ``` | |
29 | * | |
30 | * This is one of the main methods in this package. It sends a DNS query | |
31 | * for the given $domain name to your DNS server and returns a single IP | |
32 | * address on success. | |
33 | * | |
34 | * If the DNS server sends a DNS response message that contains more than | |
35 | * one IP address for this query, it will randomly pick one of the IP | |
36 | * addresses from the response. If you want the full list of IP addresses | |
37 | * or want to send a different type of query, you should use the | |
38 | * [`resolveAll()`](#resolveall) method instead. | |
39 | * | |
40 | * If the DNS server sends a DNS response message that indicates an error | |
41 | * code, this method will reject with a `RecordNotFoundException`. Its | |
42 | * message and code can be used to check for the response code. | |
43 | * | |
44 | * If the DNS communication fails and the server does not respond with a | |
45 | * valid response message, this message will reject with an `Exception`. | |
46 | * | |
47 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
48 | * | |
49 | * ```php | |
50 | * $promise = $resolver->resolve('reactphp.org'); | |
51 | * | |
52 | * $promise->cancel(); | |
53 | * ``` | |
54 | * | |
55 | * @param string $domain | |
56 | * @return PromiseInterface Returns a promise which resolves with a single IP address on success or | |
57 | * rejects with an Exception on error. | |
58 | */ | |
59 | 21 | public function resolve($domain) |
60 | 22 | { |
61 | 23 | return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) { |
63 | 25 | }); |
64 | 26 | } |
65 | 27 | |
66 | /** | |
67 | * Resolves all record values for the given $domain name and query $type. | |
68 | * | |
69 | * ```php | |
70 | * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { | |
71 | * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
72 | * }); | |
73 | * | |
74 | * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { | |
75 | * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
76 | * }); | |
77 | * ``` | |
78 | * | |
79 | * This is one of the main methods in this package. It sends a DNS query | |
80 | * for the given $domain name to your DNS server and returns a list with all | |
81 | * record values on success. | |
82 | * | |
83 | * If the DNS server sends a DNS response message that contains one or more | |
84 | * records for this query, it will return a list with all record values | |
85 | * from the response. You can use the `Message::TYPE_*` constants to control | |
86 | * which type of query will be sent. Note that this method always returns a | |
87 | * list of record values, but each record value type depends on the query | |
88 | * type. For example, it returns the IPv4 addresses for type `A` queries, | |
89 | * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, | |
90 | * `CNAME` and `PTR` queries and structured data for other queries. See also | |
91 | * the `Record` documentation for more details. | |
92 | * | |
93 | * If the DNS server sends a DNS response message that indicates an error | |
94 | * code, this method will reject with a `RecordNotFoundException`. Its | |
95 | * message and code can be used to check for the response code. | |
96 | * | |
97 | * If the DNS communication fails and the server does not respond with a | |
98 | * valid response message, this message will reject with an `Exception`. | |
99 | * | |
100 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
101 | * | |
102 | * ```php | |
103 | * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); | |
104 | * | |
105 | * $promise->cancel(); | |
106 | * ``` | |
107 | * | |
108 | * @param string $domain | |
109 | * @return PromiseInterface Returns a promise which resolves with all record values on success or | |
110 | * rejects with an Exception on error. | |
111 | */ | |
112 | 28 | public function resolveAll($domain, $type) |
113 | 29 | { |
114 | 30 | $query = new Query($domain, $type, Message::CLASS_IN); |
115 | 31 | $that = $this; |
116 | 32 | |
117 | 33 | return $this->executor->query( |
118 | $this->nameserver, | |
119 | 34 | $query |
120 | 35 | )->then(function (Message $response) use ($query, $that) { |
121 | 36 | return $that->extractValues($query, $response); |
122 | 37 | }); |
123 | } | |
124 | ||
125 | /** | |
126 | * @deprecated unused, exists for BC only | |
127 | */ | |
128 | public function extractAddress(Query $query, Message $response) | |
129 | { | |
130 | $addresses = $this->extractValues($query, $response); | |
131 | ||
132 | return $addresses[array_rand($addresses)]; | |
133 | 38 | } |
134 | 39 | |
135 | 40 | /** |
144 | 49 | public function extractValues(Query $query, Message $response) |
145 | 50 | { |
146 | 51 | // reject if response code indicates this is an error response message |
147 | $code = $response->getResponseCode(); | |
52 | $code = $response->rcode; | |
148 | 53 | if ($code !== Message::RCODE_OK) { |
149 | 54 | switch ($code) { |
150 | 55 | case Message::RCODE_FORMAT_ERROR: |
182 | 87 | } |
183 | 88 | |
184 | 89 | return array_values($addresses); |
185 | } | |
186 | ||
187 | /** | |
188 | * @deprecated unused, exists for BC only | |
189 | */ | |
190 | public function resolveAliases(array $answers, $name) | |
191 | { | |
192 | return $this->valuesByNameAndType($answers, $name, Message::TYPE_A); | |
193 | 90 | } |
194 | 91 | |
195 | 92 | /** |
0 | <?php | |
1 | ||
2 | namespace React\Dns\Resolver; | |
3 | ||
4 | interface ResolverInterface | |
5 | { | |
6 | /** | |
7 | * Resolves the given $domain name to a single IPv4 address (type `A` query). | |
8 | * | |
9 | * ```php | |
10 | * $resolver->resolve('reactphp.org')->then(function ($ip) { | |
11 | * echo 'IP for reactphp.org is ' . $ip . PHP_EOL; | |
12 | * }); | |
13 | * ``` | |
14 | * | |
15 | * This is one of the main methods in this package. It sends a DNS query | |
16 | * for the given $domain name to your DNS server and returns a single IP | |
17 | * address on success. | |
18 | * | |
19 | * If the DNS server sends a DNS response message that contains more than | |
20 | * one IP address for this query, it will randomly pick one of the IP | |
21 | * addresses from the response. If you want the full list of IP addresses | |
22 | * or want to send a different type of query, you should use the | |
23 | * [`resolveAll()`](#resolveall) method instead. | |
24 | * | |
25 | * If the DNS server sends a DNS response message that indicates an error | |
26 | * code, this method will reject with a `RecordNotFoundException`. Its | |
27 | * message and code can be used to check for the response code. | |
28 | * | |
29 | * If the DNS communication fails and the server does not respond with a | |
30 | * valid response message, this message will reject with an `Exception`. | |
31 | * | |
32 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
33 | * | |
34 | * ```php | |
35 | * $promise = $resolver->resolve('reactphp.org'); | |
36 | * | |
37 | * $promise->cancel(); | |
38 | * ``` | |
39 | * | |
40 | * @param string $domain | |
41 | * @return \React\Promise\PromiseInterface<string,\Exception> | |
42 | * resolves with a single IP address on success or rejects with an Exception on error. | |
43 | */ | |
44 | public function resolve($domain); | |
45 | ||
46 | /** | |
47 | * Resolves all record values for the given $domain name and query $type. | |
48 | * | |
49 | * ```php | |
50 | * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) { | |
51 | * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
52 | * }); | |
53 | * | |
54 | * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) { | |
55 | * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL; | |
56 | * }); | |
57 | * ``` | |
58 | * | |
59 | * This is one of the main methods in this package. It sends a DNS query | |
60 | * for the given $domain name to your DNS server and returns a list with all | |
61 | * record values on success. | |
62 | * | |
63 | * If the DNS server sends a DNS response message that contains one or more | |
64 | * records for this query, it will return a list with all record values | |
65 | * from the response. You can use the `Message::TYPE_*` constants to control | |
66 | * which type of query will be sent. Note that this method always returns a | |
67 | * list of record values, but each record value type depends on the query | |
68 | * type. For example, it returns the IPv4 addresses for type `A` queries, | |
69 | * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`, | |
70 | * `CNAME` and `PTR` queries and structured data for other queries. See also | |
71 | * the `Record` documentation for more details. | |
72 | * | |
73 | * If the DNS server sends a DNS response message that indicates an error | |
74 | * code, this method will reject with a `RecordNotFoundException`. Its | |
75 | * message and code can be used to check for the response code. | |
76 | * | |
77 | * If the DNS communication fails and the server does not respond with a | |
78 | * valid response message, this message will reject with an `Exception`. | |
79 | * | |
80 | * Pending DNS queries can be cancelled by cancelling its pending promise like so: | |
81 | * | |
82 | * ```php | |
83 | * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA); | |
84 | * | |
85 | * $promise->cancel(); | |
86 | * ``` | |
87 | * | |
88 | * @param string $domain | |
89 | * @return \React\Promise\PromiseInterface<array,\Exception> | |
90 | * Resolves with all record values on success or rejects with an Exception on error. | |
91 | */ | |
92 | public function resolveAll($domain, $type); | |
93 | } |
98 | 98 | public function testLoadsFromWmicOnWindows() |
99 | 99 | { |
100 | 100 | if (DIRECTORY_SEPARATOR !== '\\') { |
101 | $this->markTestSkipped('Only on Windows'); | |
101 | // WMIC is Windows-only tool and not supported on other platforms | |
102 | // Unix is our main platform, so we don't want to report a skipped test here (yellow) | |
103 | // $this->markTestSkipped('Only on Windows'); | |
104 | $this->expectOutputString(''); | |
105 | return; | |
102 | 106 | } |
103 | 107 | |
104 | 108 | $config = Config::loadWmicBlocking(); |
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 | } |
46 | 46 | /** |
47 | 47 | * @group internet |
48 | 48 | */ |
49 | public function testResolveGoogleOverUdpResolves() | |
50 | { | |
51 | $factory = new Factory($this->loop); | |
52 | $this->resolver = $factory->create('udp://8.8.8.8', $this->loop); | |
53 | ||
54 | $promise = $this->resolver->resolve('google.com'); | |
55 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); | |
56 | ||
57 | $this->loop->run(); | |
58 | } | |
59 | ||
60 | /** | |
61 | * @group internet | |
62 | */ | |
63 | public function testResolveGoogleOverTcpResolves() | |
64 | { | |
65 | $factory = new Factory($this->loop); | |
66 | $this->resolver = $factory->create('tcp://8.8.8.8', $this->loop); | |
67 | ||
68 | $promise = $this->resolver->resolve('google.com'); | |
69 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); | |
70 | ||
71 | $this->loop->run(); | |
72 | } | |
73 | ||
74 | /** | |
75 | * @group internet | |
76 | */ | |
49 | 77 | public function testResolveAllGoogleMxResolvesWithCache() |
50 | 78 | { |
51 | 79 | $factory = new Factory(); |
52 | 80 | $this->resolver = $factory->createCached('8.8.8.8', $this->loop); |
53 | 81 | |
54 | 82 | $promise = $this->resolver->resolveAll('google.com', Message::TYPE_MX); |
83 | $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); | |
84 | ||
85 | $this->loop->run(); | |
86 | } | |
87 | /** | |
88 | * @group internet | |
89 | */ | |
90 | public function testResolveAllGoogleCaaResolvesWithCache() | |
91 | { | |
92 | $factory = new Factory(); | |
93 | $this->resolver = $factory->createCached('8.8.8.8', $this->loop); | |
94 | ||
95 | $promise = $this->resolver->resolveAll('google.com', Message::TYPE_CAA); | |
55 | 96 | $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); |
56 | 97 | |
57 | 98 | $this->loop->run(); |
97 | 138 | $promise = $this->resolver->resolve('google.com'); |
98 | 139 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); |
99 | 140 | } |
141 | ||
142 | public function testResolveShouldNotCauseGarbageReferencesWhenUsingInvalidNameserver() | |
143 | { | |
144 | if (class_exists('React\Promise\When')) { | |
145 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
146 | } | |
147 | ||
148 | $factory = new Factory(); | |
149 | $this->resolver = $factory->create('255.255.255.255', $this->loop); | |
150 | ||
151 | gc_collect_cycles(); | |
152 | ||
153 | $promise = $this->resolver->resolve('google.com'); | |
154 | unset($promise); | |
155 | ||
156 | $this->assertEquals(0, gc_collect_cycles()); | |
157 | } | |
158 | ||
159 | public function testResolveCachedShouldNotCauseGarbageReferencesWhenUsingInvalidNameserver() | |
160 | { | |
161 | if (class_exists('React\Promise\When')) { | |
162 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
163 | } | |
164 | ||
165 | $factory = new Factory(); | |
166 | $this->resolver = $factory->createCached('255.255.255.255', $this->loop); | |
167 | ||
168 | gc_collect_cycles(); | |
169 | ||
170 | $promise = $this->resolver->resolve('google.com'); | |
171 | unset($promise); | |
172 | ||
173 | $this->assertEquals(0, gc_collect_cycles()); | |
174 | } | |
175 | ||
176 | public function testCancelResolveShouldNotCauseGarbageReferences() | |
177 | { | |
178 | if (class_exists('React\Promise\When')) { | |
179 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
180 | } | |
181 | ||
182 | $factory = new Factory(); | |
183 | $this->resolver = $factory->create('127.0.0.1', $this->loop); | |
184 | ||
185 | gc_collect_cycles(); | |
186 | ||
187 | $promise = $this->resolver->resolve('google.com'); | |
188 | $promise->cancel(); | |
189 | $promise = null; | |
190 | ||
191 | $this->assertEquals(0, gc_collect_cycles()); | |
192 | } | |
193 | ||
194 | public function testCancelResolveCachedShouldNotCauseGarbageReferences() | |
195 | { | |
196 | if (class_exists('React\Promise\When')) { | |
197 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
198 | } | |
199 | ||
200 | $factory = new Factory(); | |
201 | $this->resolver = $factory->createCached('127.0.0.1', $this->loop); | |
202 | ||
203 | gc_collect_cycles(); | |
204 | ||
205 | $promise = $this->resolver->resolve('google.com'); | |
206 | $promise->cancel(); | |
207 | $promise = null; | |
208 | ||
209 | $this->assertEquals(0, gc_collect_cycles()); | |
210 | } | |
100 | 211 | } |
9 | 9 | { |
10 | 10 | public function testCreateRequestDesiresRecusion() |
11 | 11 | { |
12 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
12 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
13 | 13 | $request = Message::createRequestForQuery($query); |
14 | 14 | |
15 | $this->assertTrue($request->header->isQuery()); | |
16 | $this->assertSame(1, $request->header->get('rd')); | |
15 | $this->assertFalse($request->qr); | |
16 | $this->assertTrue($request->rd); | |
17 | 17 | } |
18 | 18 | |
19 | 19 | public function testCreateResponseWithNoAnswers() |
20 | 20 | { |
21 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
21 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
22 | 22 | $answers = array(); |
23 | 23 | $request = Message::createResponseWithAnswersForQuery($query, $answers); |
24 | 24 | |
25 | $this->assertFalse($request->header->isQuery()); | |
26 | $this->assertTrue($request->header->isResponse()); | |
27 | $this->assertEquals(0, $request->header->get('anCount')); | |
28 | $this->assertEquals(Message::RCODE_OK, $request->getResponseCode()); | |
25 | $this->assertTrue($request->qr); | |
26 | $this->assertEquals(Message::RCODE_OK, $request->rcode); | |
29 | 27 | } |
30 | 28 | } |
2 | 2 | namespace React\Tests\Dns\Protocol; |
3 | 3 | |
4 | 4 | use PHPUnit\Framework\TestCase; |
5 | use React\Dns\Model\Message; | |
6 | use React\Dns\Model\Record; | |
5 | 7 | use React\Dns\Protocol\BinaryDumper; |
6 | use React\Dns\Model\Message; | |
8 | use React\Dns\Query\Query; | |
7 | 9 | |
8 | 10 | class BinaryDumperTest extends TestCase |
9 | 11 | { |
10 | public function testRequestToBinary() | |
12 | public function testToBinaryRequestMessage() | |
11 | 13 | { |
12 | 14 | $data = ""; |
13 | 15 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header |
14 | 16 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io |
15 | 17 | $data .= "00 01 00 01"; // question: type A, class IN |
16 | 18 | |
17 | $expected = $this->formatHexDump(str_replace(' ', '', $data), 2); | |
19 | $expected = $this->formatHexDump($data); | |
18 | 20 | |
19 | 21 | $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(); | |
22 | $request->id = 0x7262; | |
23 | $request->rd = true; | |
24 | ||
25 | $request->questions[] = new Query( | |
26 | 'igor.io', | |
27 | Message::TYPE_A, | |
28 | Message::CLASS_IN | |
29 | ); | |
30 | 30 | |
31 | 31 | $dumper = new BinaryDumper(); |
32 | 32 | $data = $dumper->toBinary($request); |
35 | 35 | $this->assertSame($expected, $data); |
36 | 36 | } |
37 | 37 | |
38 | public function testToBinaryRequestMessageWithCustomOptForEdns0() | |
39 | { | |
40 | $data = ""; | |
41 | $data .= "72 62 01 00 00 01 00 00 00 00 00 01"; // header | |
42 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
43 | $data .= "00 01 00 01"; // question: type A, class IN | |
44 | $data .= "00"; // additional: (empty hostname) | |
45 | $data .= "00 29 03 e8 00 00 00 00 00 00 "; // additional: type OPT, class UDP size, TTL 0, no RDATA | |
46 | ||
47 | $expected = $this->formatHexDump($data); | |
48 | ||
49 | $request = new Message(); | |
50 | $request->id = 0x7262; | |
51 | $request->rd = true; | |
52 | ||
53 | $request->questions[] = new Query( | |
54 | 'igor.io', | |
55 | Message::TYPE_A, | |
56 | Message::CLASS_IN | |
57 | ); | |
58 | ||
59 | $request->additional[] = new Record('', 41, 1000, 0, ''); | |
60 | ||
61 | $dumper = new BinaryDumper(); | |
62 | $data = $dumper->toBinary($request); | |
63 | $data = $this->convertBinaryToHexDump($data); | |
64 | ||
65 | $this->assertSame($expected, $data); | |
66 | } | |
67 | ||
68 | public function testToBinaryResponseMessageWithoutRecords() | |
69 | { | |
70 | $data = ""; | |
71 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
72 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
73 | $data .= "00 01 00 01"; // question: type A, class IN | |
74 | ||
75 | $expected = $this->formatHexDump($data); | |
76 | ||
77 | $response = new Message(); | |
78 | $response->id = 0x7262; | |
79 | $response->rd = true; | |
80 | $response->rcode = Message::RCODE_OK; | |
81 | ||
82 | $response->questions[] = new Query( | |
83 | 'igor.io', | |
84 | Message::TYPE_A, | |
85 | Message::CLASS_IN | |
86 | ); | |
87 | ||
88 | $dumper = new BinaryDumper(); | |
89 | $data = $dumper->toBinary($response); | |
90 | $data = $this->convertBinaryToHexDump($data); | |
91 | ||
92 | $this->assertSame($expected, $data); | |
93 | } | |
94 | ||
95 | public function testToBinaryForResponseWithSRVRecord() | |
96 | { | |
97 | $data = ""; | |
98 | $data .= "72 62 01 00 00 01 00 01 00 00 00 00"; // header | |
99 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
100 | $data .= "00 21 00 01"; // question: type SRV, class IN | |
101 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
102 | $data .= "00 21 00 01"; // answer: type SRV, class IN | |
103 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
104 | $data .= "00 0c"; // answer: rdlength 12 | |
105 | $data .= "00 0a 00 14 1f 90 04 74 65 73 74 00"; // answer: rdata priority 10, weight 20, port 8080 test | |
106 | ||
107 | $expected = $this->formatHexDump($data); | |
108 | ||
109 | $response = new Message(); | |
110 | $response->id = 0x7262; | |
111 | $response->rd = true; | |
112 | $response->rcode = Message::RCODE_OK; | |
113 | ||
114 | $response->questions[] = new Query( | |
115 | 'igor.io', | |
116 | Message::TYPE_SRV, | |
117 | Message::CLASS_IN | |
118 | ); | |
119 | ||
120 | $response->answers[] = new Record('igor.io', Message::TYPE_SRV, Message::CLASS_IN, 86400, array( | |
121 | 'priority' => 10, | |
122 | 'weight' => 20, | |
123 | 'port' => 8080, | |
124 | 'target' => 'test' | |
125 | )); | |
126 | ||
127 | $dumper = new BinaryDumper(); | |
128 | $data = $dumper->toBinary($response); | |
129 | $data = $this->convertBinaryToHexDump($data); | |
130 | ||
131 | $this->assertSame($expected, $data); | |
132 | } | |
133 | ||
134 | public function testToBinaryForResponseWithSOARecord() | |
135 | { | |
136 | $data = ""; | |
137 | $data .= "72 62 01 00 00 01 00 01 00 00 00 00"; // header | |
138 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
139 | $data .= "00 06 00 01"; // question: type SOA, class IN | |
140 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
141 | $data .= "00 06 00 01"; // answer: type SOA, class IN | |
142 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
143 | $data .= "00 27"; // answer: rdlength 39 | |
144 | $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) | |
145 | $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) | |
146 | $data .= "78 49 28 d5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600 | |
147 | $data .= "00 09 3e 68 00 00 0e 10"; // answer: 605800, 3600 | |
148 | ||
149 | $expected = $this->formatHexDump($data); | |
150 | ||
151 | $response = new Message(); | |
152 | $response->id = 0x7262; | |
153 | $response->rd = true; | |
154 | $response->rcode = Message::RCODE_OK; | |
155 | ||
156 | $response->questions[] = new Query( | |
157 | 'igor.io', | |
158 | Message::TYPE_SOA, | |
159 | Message::CLASS_IN | |
160 | ); | |
161 | ||
162 | $response->answers[] = new Record('igor.io', Message::TYPE_SOA, Message::CLASS_IN, 86400, array( | |
163 | 'mname' => 'ns.hello', | |
164 | 'rname' => 'e.hello', | |
165 | 'serial' => 2018060501, | |
166 | 'refresh' => 10800, | |
167 | 'retry' => 3600, | |
168 | 'expire' => 605800, | |
169 | 'minimum' => 3600 | |
170 | )); | |
171 | ||
172 | $dumper = new BinaryDumper(); | |
173 | $data = $dumper->toBinary($response); | |
174 | $data = $this->convertBinaryToHexDump($data); | |
175 | ||
176 | $this->assertSame($expected, $data); | |
177 | } | |
178 | ||
179 | public function testToBinaryForResponseWithPTRRecordWithSpecialCharactersEscaped() | |
180 | { | |
181 | $data = ""; | |
182 | $data .= "72 62 01 00 00 01 00 01 00 00 00 00"; // header | |
183 | $data .= "08 5f 70 72 69 6e 74 65 72 04 5f 74 63 70 06 64 6e 73 2d 73 64 03 6f 72 67 00"; // question: _printer._tcp.dns-sd.org | |
184 | $data .= "00 0c 00 01"; // question: type PTR, class IN | |
185 | $data .= "08 5f 70 72 69 6e 74 65 72 04 5f 74 63 70 06 64 6e 73 2d 73 64 03 6f 72 67 00"; // answer: _printer._tcp.dns-sd.org | |
186 | $data .= "00 0c 00 01"; // answer: type PTR, class IN | |
187 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
188 | $data .= "00 2f"; // answer: rdlength 47 | |
189 | $data .= "14 33 72 64 2e 20 46 6c 6f 6f 72 20 43 6f 70 79 20 52 6f 6f 6d"; // answer: answer: rdata "3rd. Floor Copy Room" … | |
190 | $data .= "08 5f 70 72 69 6e 74 65 72 04 5f 74 63 70 06 64 6e 73 2d 73 64 03 6f 72 67 00"; // answer: … "._printer._tcp.dns-sd.org" | |
191 | ||
192 | $expected = $this->formatHexDump($data); | |
193 | ||
194 | $response = new Message(); | |
195 | $response->id = 0x7262; | |
196 | $response->rd = true; | |
197 | $response->rcode = Message::RCODE_OK; | |
198 | ||
199 | $response->questions[] = new Query( | |
200 | '_printer._tcp.dns-sd.org', | |
201 | Message::TYPE_PTR, | |
202 | Message::CLASS_IN | |
203 | ); | |
204 | ||
205 | $response->answers[] = new Record( | |
206 | '_printer._tcp.dns-sd.org', | |
207 | Message::TYPE_PTR, | |
208 | Message::CLASS_IN, | |
209 | 86400, | |
210 | '3rd\.\ Floor\ Copy\ Room._printer._tcp.dns-sd.org' | |
211 | ); | |
212 | ||
213 | $dumper = new BinaryDumper(); | |
214 | $data = $dumper->toBinary($response); | |
215 | $data = $this->convertBinaryToHexDump($data); | |
216 | ||
217 | $this->assertSame($expected, $data); | |
218 | } | |
219 | ||
220 | public function testToBinaryForResponseWithMultipleAnswerRecords() | |
221 | { | |
222 | $data = ""; | |
223 | $data .= "72 62 01 00 00 01 00 06 00 00 00 00"; // header | |
224 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
225 | $data .= "00 ff 00 01"; // question: type ANY, class IN | |
226 | ||
227 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
228 | $data .= "00 01 00 01 00 00 00 00 00 04"; // answer: type A, class IN, TTL 0, 4 bytes | |
229 | $data .= "7f 00 00 01"; // answer: 127.0.0.1 | |
230 | ||
231 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
232 | $data .= "00 1c 00 01 00 00 00 00 00 10"; // question: type AAAA, class IN, TTL 0, 16 bytes | |
233 | $data .= "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01"; // answer: ::1 | |
234 | ||
235 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
236 | $data .= "00 10 00 01 00 00 00 00 00 0c"; // answer: type TXT, class IN, TTL 0, 12 bytes | |
237 | $data .= "05 68 65 6c 6c 6f 05 77 6f 72 6c 64"; // answer: hello, world | |
238 | ||
239 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
240 | $data .= "00 0f 00 01 00 00 00 00 00 03"; // answer: type MX, class IN, TTL 0, 3 bytes | |
241 | $data .= "00 00 00"; // answer: … priority 0, no target | |
242 | ||
243 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io … | |
244 | $data .= "01 01 00 01 00 00 00 00 00 16"; // answer: type CAA, class IN, TTL 0, 22 bytes | |
245 | $data .= "00 05 69 73 73 75 65"; // answer: 0 issue … | |
246 | $data .= "6c 65 74 73 65 6e 63 72 79 70 74 2e 6f 72 67"; // answer: … letsencrypt.org | |
247 | ||
248 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io … | |
249 | $data .= "00 2c 00 01 00 00 00 00 00 06"; // answer: type SSHFP, class IN, TTL 0, 6 bytes | |
250 | $data .= "01 01 69 ac 09 0c"; // answer: algorithm 1 (RSA), type 1 (SHA-1), fingerprint "69ac090c" | |
251 | ||
252 | $expected = $this->formatHexDump($data); | |
253 | ||
254 | $response = new Message(); | |
255 | $response->id = 0x7262; | |
256 | $response->rd = true; | |
257 | $response->rcode = Message::RCODE_OK; | |
258 | ||
259 | $response->questions[] = new Query( | |
260 | 'igor.io', | |
261 | Message::TYPE_ANY, | |
262 | Message::CLASS_IN | |
263 | ); | |
264 | ||
265 | $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 0, '127.0.0.1'); | |
266 | $response->answers[] = new Record('igor.io', Message::TYPE_AAAA, Message::CLASS_IN, 0, '::1'); | |
267 | $response->answers[] = new Record('igor.io', Message::TYPE_TXT, Message::CLASS_IN, 0, array('hello', 'world')); | |
268 | $response->answers[] = new Record('igor.io', Message::TYPE_MX, Message::CLASS_IN, 0, array('priority' => 0, 'target' => '')); | |
269 | $response->answers[] = new Record('igor.io', Message::TYPE_CAA, Message::CLASS_IN, 0, array('flag' => 0, 'tag' => 'issue', 'value' => 'letsencrypt.org')); | |
270 | $response->answers[] = new Record('igor.io', Message::TYPE_SSHFP, Message::CLASS_IN, 0, array('algorithm' => 1, 'type' => '1', 'fingerprint' => '69ac090c')); | |
271 | ||
272 | $dumper = new BinaryDumper(); | |
273 | $data = $dumper->toBinary($response); | |
274 | $data = $this->convertBinaryToHexDump($data); | |
275 | ||
276 | $this->assertSame($expected, $data); | |
277 | } | |
278 | ||
279 | public function testToBinaryForResponseWithAnswerAndAdditionalRecord() | |
280 | { | |
281 | $data = ""; | |
282 | $data .= "72 62 01 00 00 01 00 01 00 00 00 01"; // header | |
283 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
284 | $data .= "00 02 00 01"; // question: type NS, class IN | |
285 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
286 | $data .= "00 02 00 01 00 00 00 00 00 0d"; // answer: type NS, class IN, TTL 0, 10 bytes | |
287 | $data .= "07 65 78 61 6d 70 6c 65 03 63 6f 6d 00"; // answer: example.com | |
288 | $data .= "07 65 78 61 6d 70 6c 65 03 63 6f 6d 00"; // additional: example.com | |
289 | $data .= "00 01 00 01 00 00 00 00 00 04"; // additional: type A, class IN, TTL 0, 4 bytes | |
290 | $data .= "7f 00 00 01"; // additional: 127.0.0.1 | |
291 | ||
292 | $expected = $this->formatHexDump($data); | |
293 | ||
294 | $response = new Message(); | |
295 | $response->id = 0x7262; | |
296 | $response->rd = true; | |
297 | $response->rcode = Message::RCODE_OK; | |
298 | ||
299 | $response->questions[] = new Query( | |
300 | 'igor.io', | |
301 | Message::TYPE_NS, | |
302 | Message::CLASS_IN | |
303 | ); | |
304 | ||
305 | $response->answers[] = new Record('igor.io', Message::TYPE_NS, Message::CLASS_IN, 0, 'example.com'); | |
306 | $response->additional[] = new Record('example.com', Message::TYPE_A, Message::CLASS_IN, 0, '127.0.0.1'); | |
307 | ||
308 | $dumper = new BinaryDumper(); | |
309 | $data = $dumper->toBinary($response); | |
310 | $data = $this->convertBinaryToHexDump($data); | |
311 | ||
312 | $this->assertSame($expected, $data); | |
313 | } | |
314 | ||
38 | 315 | private function convertBinaryToHexDump($input) |
39 | 316 | { |
40 | 317 | return $this->formatHexDump(implode('', unpack('H*', $input))); |
42 | 319 | |
43 | 320 | private function formatHexDump($input) |
44 | 321 | { |
45 | return implode(' ', str_split($input, 2)); | |
322 | return implode(' ', str_split(str_replace(' ', '', $input), 2)); | |
46 | 323 | } |
47 | 324 | } |
41 | 41 | |
42 | 42 | $request = $this->parser->parseMessage($data); |
43 | 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')); | |
44 | $this->assertFalse(isset($request->data)); | |
45 | $this->assertFalse(isset($request->consumed)); | |
46 | ||
47 | $this->assertSame(0x7262, $request->id); | |
48 | $this->assertSame(false, $request->qr); | |
49 | $this->assertSame(Message::OPCODE_QUERY, $request->opcode); | |
50 | $this->assertSame(false, $request->aa); | |
51 | $this->assertSame(false, $request->tc); | |
52 | $this->assertSame(true, $request->rd); | |
53 | $this->assertSame(false, $request->ra); | |
54 | $this->assertSame(Message::RCODE_OK, $request->rcode); | |
58 | 55 | |
59 | 56 | $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']); | |
57 | $this->assertSame('igor.io', $request->questions[0]->name); | |
58 | $this->assertSame(Message::TYPE_A, $request->questions[0]->type); | |
59 | $this->assertSame(Message::CLASS_IN, $request->questions[0]->class); | |
63 | 60 | } |
64 | 61 | |
65 | 62 | public function testParseResponse() |
78 | 75 | |
79 | 76 | $response = $this->parser->parseMessage($data); |
80 | 77 | |
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')); | |
78 | $this->assertSame(0x7262, $response->id); | |
79 | $this->assertSame(true, $response->qr); | |
80 | $this->assertSame(Message::OPCODE_QUERY, $response->opcode); | |
81 | $this->assertSame(false, $response->aa); | |
82 | $this->assertSame(false, $response->tc); | |
83 | $this->assertSame(true, $response->rd); | |
84 | $this->assertSame(true, $response->ra); | |
85 | $this->assertSame(Message::RCODE_OK, $response->rcode); | |
95 | 86 | |
96 | 87 | $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']); | |
88 | $this->assertSame('igor.io', $response->questions[0]->name); | |
89 | $this->assertSame(Message::TYPE_A, $response->questions[0]->type); | |
90 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
100 | 91 | |
101 | 92 | $this->assertCount(1, $response->answers); |
102 | 93 | $this->assertSame('igor.io', $response->answers[0]->name); |
106 | 97 | $this->assertSame('178.79.169.131', $response->answers[0]->data); |
107 | 98 | } |
108 | 99 | |
109 | public function testParseQuestionWithTwoQuestions() | |
110 | { | |
111 | $data = ""; | |
100 | public function testParseRequestWithTwoQuestions() | |
101 | { | |
102 | $data = ""; | |
103 | $data .= "72 62 01 00 00 02 00 00 00 00 00 00"; // header | |
112 | 104 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io |
113 | 105 | $data .= "00 01 00 01"; // question: type A, class IN |
114 | 106 | $data .= "03 77 77 77 04 69 67 6f 72 02 69 6f 00"; // question: www.igor.io |
116 | 108 | |
117 | 109 | $data = $this->convertTcpDumpToBinary($data); |
118 | 110 | |
119 | $request = new Message(); | |
120 | $request->header->set('qdCount', 2); | |
121 | $request->data = $data; | |
122 | ||
123 | $this->parser->parseQuestion($request); | |
111 | $request = $this->parser->parseMessage($data); | |
124 | 112 | |
125 | 113 | $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']); | |
114 | $this->assertSame('igor.io', $request->questions[0]->name); | |
115 | $this->assertSame(Message::TYPE_A, $request->questions[0]->type); | |
116 | $this->assertSame(Message::CLASS_IN, $request->questions[0]->class); | |
117 | $this->assertSame('www.igor.io', $request->questions[1]->name); | |
118 | $this->assertSame(Message::TYPE_A, $request->questions[1]->type); | |
119 | $this->assertSame(Message::CLASS_IN, $request->questions[1]->class); | |
132 | 120 | } |
133 | 121 | |
134 | 122 | public function testParseAnswerWithInlineData() |
140 | 128 | $data .= "00 04"; // answer: rdlength 4 |
141 | 129 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 |
142 | 130 | |
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); | |
131 | $response = $this->parseAnswer($data); | |
150 | 132 | |
151 | 133 | $this->assertCount(1, $response->answers); |
152 | 134 | $this->assertSame('igor.io', $response->answers[0]->name); |
165 | 147 | $data .= "00 04"; // answer: rdlength 4 |
166 | 148 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 |
167 | 149 | |
168 | $data = $this->convertTcpDumpToBinary($data); | |
169 | ||
170 | $response = new Message(); | |
171 | $response->header->set('anCount', 1); | |
172 | $response->data = $data; | |
173 | ||
174 | $this->parser->parseAnswer($response); | |
150 | $response = $this->parseAnswer($data); | |
175 | 151 | |
176 | 152 | $this->assertCount(1, $response->answers); |
177 | 153 | $this->assertSame('igor.io', $response->answers[0]->name); |
190 | 166 | $data .= "00 04"; // answer: rdlength 4 |
191 | 167 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 |
192 | 168 | |
193 | $data = $this->convertTcpDumpToBinary($data); | |
194 | ||
195 | $response = new Message(); | |
196 | $response->header->set('anCount', 1); | |
197 | $response->data = $data; | |
198 | ||
199 | $this->parser->parseAnswer($response); | |
169 | $response = $this->parseAnswer($data); | |
200 | 170 | |
201 | 171 | $this->assertCount(1, $response->answers); |
202 | 172 | $this->assertSame('igor.io', $response->answers[0]->name); |
215 | 185 | $data .= "00 04"; // answer: rdlength 4 |
216 | 186 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 |
217 | 187 | |
218 | $data = $this->convertTcpDumpToBinary($data); | |
219 | ||
220 | $response = new Message(); | |
221 | $response->header->set('anCount', 1); | |
222 | $response->data = $data; | |
223 | ||
224 | $this->parser->parseAnswer($response); | |
188 | $response = $this->parseAnswer($data); | |
225 | 189 | |
226 | 190 | $this->assertCount(1, $response->answers); |
227 | 191 | $this->assertSame('igor.io', $response->answers[0]->name); |
240 | 204 | $data .= "00 05"; // answer: rdlength 5 |
241 | 205 | $data .= "68 65 6c 6c 6f"; // answer: rdata "hello" |
242 | 206 | |
243 | $data = $this->convertTcpDumpToBinary($data); | |
244 | ||
245 | $response = new Message(); | |
246 | $response->header->set('anCount', 1); | |
247 | $response->data = $data; | |
248 | ||
249 | $this->parser->parseAnswer($response); | |
207 | $response = $this->parseAnswer($data); | |
250 | 208 | |
251 | 209 | $this->assertCount(1, $response->answers); |
252 | 210 | $this->assertSame('igor.io', $response->answers[0]->name); |
274 | 232 | $response = $this->parser->parseMessage($data); |
275 | 233 | |
276 | 234 | $this->assertCount(1, $response->questions); |
277 | $this->assertSame('mail.google.com', $response->questions[0]['name']); | |
278 | $this->assertSame(Message::TYPE_CNAME, $response->questions[0]['type']); | |
279 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
235 | $this->assertSame('mail.google.com', $response->questions[0]->name); | |
236 | $this->assertSame(Message::TYPE_CNAME, $response->questions[0]->type); | |
237 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
280 | 238 | |
281 | 239 | $this->assertCount(1, $response->answers); |
282 | 240 | $this->assertSame('mail.google.com', $response->answers[0]->name); |
302 | 260 | |
303 | 261 | $response = $this->parser->parseMessage($data); |
304 | 262 | |
305 | $header = $response->header; | |
306 | $this->assertSame(0xcd72, $header->get('id')); | |
307 | $this->assertSame(1, $header->get('qdCount')); | |
308 | $this->assertSame(1, $header->get('anCount')); | |
309 | $this->assertSame(0, $header->get('nsCount')); | |
310 | $this->assertSame(0, $header->get('arCount')); | |
311 | $this->assertSame(1, $header->get('qr')); | |
312 | $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); | |
313 | $this->assertSame(0, $header->get('aa')); | |
314 | $this->assertSame(0, $header->get('tc')); | |
315 | $this->assertSame(1, $header->get('rd')); | |
316 | $this->assertSame(1, $header->get('ra')); | |
317 | $this->assertSame(0, $header->get('z')); | |
318 | $this->assertSame(Message::RCODE_OK, $header->get('rcode')); | |
263 | $this->assertSame(0xcd72, $response->id); | |
264 | $this->assertSame(true, $response->qr); | |
265 | $this->assertSame(Message::OPCODE_QUERY, $response->opcode); | |
266 | $this->assertSame(false, $response->aa); | |
267 | $this->assertSame(false, $response->tc); | |
268 | $this->assertSame(true, $response->rd); | |
269 | $this->assertSame(true, $response->ra); | |
270 | $this->assertSame(Message::RCODE_OK, $response->rcode); | |
319 | 271 | |
320 | 272 | $this->assertCount(1, $response->questions); |
321 | $this->assertSame('google.com', $response->questions[0]['name']); | |
322 | $this->assertSame(Message::TYPE_AAAA, $response->questions[0]['type']); | |
323 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
273 | $this->assertSame('google.com', $response->questions[0]->name); | |
274 | $this->assertSame(Message::TYPE_AAAA, $response->questions[0]->type); | |
275 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
324 | 276 | |
325 | 277 | $this->assertCount(1, $response->answers); |
326 | 278 | $this->assertSame('google.com', $response->answers[0]->name); |
339 | 291 | $data .= "00 06"; // answer: rdlength 6 |
340 | 292 | $data .= "05 68 65 6c 6c 6f"; // answer: rdata length 5: hello |
341 | 293 | |
342 | $data = $this->convertTcpDumpToBinary($data); | |
343 | ||
344 | $response = new Message(); | |
345 | $response->header->set('anCount', 1); | |
346 | $response->data = $data; | |
347 | ||
348 | $this->parser->parseAnswer($response); | |
294 | $response = $this->parseAnswer($data); | |
349 | 295 | |
350 | 296 | $this->assertCount(1, $response->answers); |
351 | 297 | $this->assertSame('igor.io', $response->answers[0]->name); |
364 | 310 | $data .= "00 0C"; // answer: rdlength 12 |
365 | 311 | $data .= "05 68 65 6c 6c 6f 05 77 6f 72 6c 64"; // answer: rdata length 5: hello, length 5: world |
366 | 312 | |
367 | $data = $this->convertTcpDumpToBinary($data); | |
368 | ||
369 | $response = new Message(); | |
370 | $response->header->set('anCount', 1); | |
371 | $response->data = $data; | |
372 | ||
373 | $this->parser->parseAnswer($response); | |
313 | $response = $this->parseAnswer($data); | |
374 | 314 | |
375 | 315 | $this->assertCount(1, $response->answers); |
376 | 316 | $this->assertSame('igor.io', $response->answers[0]->name); |
389 | 329 | $data .= "00 09"; // answer: rdlength 9 |
390 | 330 | $data .= "00 0a 05 68 65 6c 6c 6f 00"; // answer: rdata priority 10: hello |
391 | 331 | |
392 | $data = $this->convertTcpDumpToBinary($data); | |
393 | ||
394 | $response = new Message(); | |
395 | $response->header->set('anCount', 1); | |
396 | $response->data = $data; | |
397 | ||
398 | $this->parser->parseAnswer($response); | |
332 | $response = $this->parseAnswer($data); | |
399 | 333 | |
400 | 334 | $this->assertCount(1, $response->answers); |
401 | 335 | $this->assertSame('igor.io', $response->answers[0]->name); |
414 | 348 | $data .= "00 0C"; // answer: rdlength 12 |
415 | 349 | $data .= "00 0a 00 14 1F 90 04 74 65 73 74 00"; // answer: rdata priority 10, weight 20, port 8080 test |
416 | 350 | |
417 | $data = $this->convertTcpDumpToBinary($data); | |
418 | ||
419 | $response = new Message(); | |
420 | $response->header->set('anCount', 1); | |
421 | $response->data = $data; | |
422 | ||
423 | $this->parser->parseAnswer($response); | |
351 | $response = $this->parseAnswer($data); | |
424 | 352 | |
425 | 353 | $this->assertCount(1, $response->answers); |
426 | 354 | $this->assertSame('igor.io', $response->answers[0]->name); |
438 | 366 | ); |
439 | 367 | } |
440 | 368 | |
441 | public function testParseResponseWithTwoAnswers() | |
369 | public function testParseMessageResponseWithTwoAnswers() | |
442 | 370 | { |
443 | 371 | $data = ""; |
444 | 372 | $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header |
461 | 389 | $response = $this->parser->parseMessage($data); |
462 | 390 | |
463 | 391 | $this->assertCount(1, $response->questions); |
464 | $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); | |
465 | $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); | |
466 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
392 | $this->assertSame('io.whois-servers.net', $response->questions[0]->name); | |
393 | $this->assertSame(Message::TYPE_A, $response->questions[0]->type); | |
394 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
467 | 395 | |
468 | 396 | $this->assertCount(2, $response->answers); |
469 | 397 | |
480 | 408 | $this->assertSame('193.223.78.152', $response->answers[1]->data); |
481 | 409 | } |
482 | 410 | |
411 | public function testParseMessageResponseWithTwoAuthorityRecords() | |
412 | { | |
413 | $data = ""; | |
414 | $data .= "bc 73 81 80 00 01 00 00 00 02 00 00"; // header | |
415 | $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; | |
416 | // question: io.whois-servers.net | |
417 | $data .= "00 01 00 01"; // question: type A, class IN | |
418 | $data .= "c0 0c"; // authority: offset pointer to io.whois-servers.net | |
419 | $data .= "00 05 00 01"; // authority: type CNAME, class IN | |
420 | $data .= "00 00 00 29"; // authority: ttl 41 | |
421 | $data .= "00 0e"; // authority: rdlength 14 | |
422 | $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // authority: rdata whois.nic.io | |
423 | $data .= "c0 32"; // authority: offset pointer to whois.nic.io | |
424 | $data .= "00 01 00 01"; // authority: type CNAME, class IN | |
425 | $data .= "00 00 0d f7"; // authority: ttl 3575 | |
426 | $data .= "00 04"; // authority: rdlength 4 | |
427 | $data .= "c1 df 4e 98"; // authority: rdata 193.223.78.152 | |
428 | ||
429 | $data = $this->convertTcpDumpToBinary($data); | |
430 | ||
431 | $response = $this->parser->parseMessage($data); | |
432 | ||
433 | $this->assertCount(1, $response->questions); | |
434 | $this->assertSame('io.whois-servers.net', $response->questions[0]->name); | |
435 | $this->assertSame(Message::TYPE_A, $response->questions[0]->type); | |
436 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
437 | ||
438 | $this->assertCount(0, $response->answers); | |
439 | ||
440 | $this->assertCount(2, $response->authority); | |
441 | ||
442 | $this->assertSame('io.whois-servers.net', $response->authority[0]->name); | |
443 | $this->assertSame(Message::TYPE_CNAME, $response->authority[0]->type); | |
444 | $this->assertSame(Message::CLASS_IN, $response->authority[0]->class); | |
445 | $this->assertSame(41, $response->authority[0]->ttl); | |
446 | $this->assertSame('whois.nic.io', $response->authority[0]->data); | |
447 | ||
448 | $this->assertSame('whois.nic.io', $response->authority[1]->name); | |
449 | $this->assertSame(Message::TYPE_A, $response->authority[1]->type); | |
450 | $this->assertSame(Message::CLASS_IN, $response->authority[1]->class); | |
451 | $this->assertSame(3575, $response->authority[1]->ttl); | |
452 | $this->assertSame('193.223.78.152', $response->authority[1]->data); | |
453 | } | |
454 | ||
455 | public function testParseMessageResponseWithAnswerAndAdditionalRecord() | |
456 | { | |
457 | $data = ""; | |
458 | $data .= "bc 73 81 80 00 01 00 01 00 00 00 01"; // header | |
459 | $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; | |
460 | // question: io.whois-servers.net | |
461 | $data .= "00 01 00 01"; // question: type A, class IN | |
462 | $data .= "c0 0c"; // answer: offset pointer to io.whois-servers.net | |
463 | $data .= "00 05 00 01"; // answer: type CNAME, class IN | |
464 | $data .= "00 00 00 29"; // answer: ttl 41 | |
465 | $data .= "00 0e"; // answer: rdlength 14 | |
466 | $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // answer: rdata whois.nic.io | |
467 | $data .= "c0 32"; // additional: offset pointer to whois.nic.io | |
468 | $data .= "00 01 00 01"; // additional: type CNAME, class IN | |
469 | $data .= "00 00 0d f7"; // additional: ttl 3575 | |
470 | $data .= "00 04"; // additional: rdlength 4 | |
471 | $data .= "c1 df 4e 98"; // additional: rdata 193.223.78.152 | |
472 | ||
473 | $data = $this->convertTcpDumpToBinary($data); | |
474 | ||
475 | $response = $this->parser->parseMessage($data); | |
476 | ||
477 | $this->assertCount(1, $response->questions); | |
478 | $this->assertSame('io.whois-servers.net', $response->questions[0]->name); | |
479 | $this->assertSame(Message::TYPE_A, $response->questions[0]->type); | |
480 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
481 | ||
482 | $this->assertCount(1, $response->answers); | |
483 | ||
484 | $this->assertSame('io.whois-servers.net', $response->answers[0]->name); | |
485 | $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); | |
486 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
487 | $this->assertSame(41, $response->answers[0]->ttl); | |
488 | $this->assertSame('whois.nic.io', $response->answers[0]->data); | |
489 | ||
490 | $this->assertCount(0, $response->authority); | |
491 | $this->assertCount(1, $response->additional); | |
492 | ||
493 | $this->assertSame('whois.nic.io', $response->additional[0]->name); | |
494 | $this->assertSame(Message::TYPE_A, $response->additional[0]->type); | |
495 | $this->assertSame(Message::CLASS_IN, $response->additional[0]->class); | |
496 | $this->assertSame(3575, $response->additional[0]->ttl); | |
497 | $this->assertSame('193.223.78.152', $response->additional[0]->data); | |
498 | } | |
499 | ||
483 | 500 | public function testParseNSResponse() |
484 | 501 | { |
485 | 502 | $data = ""; |
489 | 506 | $data .= "00 07"; // answer: rdlength 7 |
490 | 507 | $data .= "05 68 65 6c 6c 6f 00"; // answer: rdata hello |
491 | 508 | |
492 | $data = $this->convertTcpDumpToBinary($data); | |
493 | ||
494 | $response = new Message(); | |
495 | $response->header->set('anCount', 1); | |
496 | $response->data = $data; | |
497 | ||
498 | $this->parser->parseAnswer($response); | |
509 | $response = $this->parseAnswer($data); | |
499 | 510 | |
500 | 511 | $this->assertCount(1, $response->answers); |
501 | 512 | $this->assertSame('igor.io', $response->answers[0]->name); |
503 | 514 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); |
504 | 515 | $this->assertSame(86400, $response->answers[0]->ttl); |
505 | 516 | $this->assertSame('hello', $response->answers[0]->data); |
517 | } | |
518 | ||
519 | public function testParseSSHFPResponse() | |
520 | { | |
521 | $data = ""; | |
522 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
523 | $data .= "00 2c 00 01"; // answer: type SSHFP, class IN | |
524 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
525 | $data .= "00 06"; // answer: rdlength 6 | |
526 | $data .= "01 01 69 ac 09 0c"; // answer: algorithm 1 (RSA), type 1 (SHA-1), fingerprint "69ac090c" | |
527 | ||
528 | $response = $this->parseAnswer($data); | |
529 | ||
530 | $this->assertCount(1, $response->answers); | |
531 | $this->assertSame('igor.io', $response->answers[0]->name); | |
532 | $this->assertSame(Message::TYPE_SSHFP, $response->answers[0]->type); | |
533 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
534 | $this->assertSame(86400, $response->answers[0]->ttl); | |
535 | $this->assertSame(array('algorithm' => 1, 'type' => 1, 'fingerprint' => '69ac090c'), $response->answers[0]->data); | |
506 | 536 | } |
507 | 537 | |
508 | 538 | public function testParseSOAResponse() |
517 | 547 | $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600 |
518 | 548 | $data .= "00 09 3a 80 00 00 0e 10"; // answer: 605800, 3600 |
519 | 549 | |
520 | $data = $this->convertTcpDumpToBinary($data); | |
521 | ||
522 | $response = new Message(); | |
523 | $response->header->set('anCount', 1); | |
524 | $response->data = $data; | |
525 | ||
526 | $this->parser->parseAnswer($response); | |
550 | $response = $this->parseAnswer($data); | |
527 | 551 | |
528 | 552 | $this->assertCount(1, $response->answers); |
529 | 553 | $this->assertSame('igor.io', $response->answers[0]->name); |
544 | 568 | ); |
545 | 569 | } |
546 | 570 | |
571 | public function testParseCAAResponse() | |
572 | { | |
573 | $data = ""; | |
574 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
575 | $data .= "01 01 00 01"; // answer: type CAA, class IN | |
576 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
577 | $data .= "00 16"; // answer: rdlength 22 | |
578 | $data .= "00 05 69 73 73 75 65"; // answer: rdata 0, issue | |
579 | $data .= "6c 65 74 73 65 6e 63 72 79 70 74 2e 6f 72 67"; // answer: letsencrypt.org | |
580 | ||
581 | $response = $this->parseAnswer($data); | |
582 | ||
583 | $this->assertCount(1, $response->answers); | |
584 | $this->assertSame('igor.io', $response->answers[0]->name); | |
585 | $this->assertSame(Message::TYPE_CAA, $response->answers[0]->type); | |
586 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
587 | $this->assertSame(86400, $response->answers[0]->ttl); | |
588 | $this->assertSame(array('flag' => 0, 'tag' => 'issue', 'value' => 'letsencrypt.org'), $response->answers[0]->data); | |
589 | } | |
590 | ||
547 | 591 | public function testParsePTRResponse() |
548 | 592 | { |
549 | 593 | $data = ""; |
562 | 606 | |
563 | 607 | $response = $this->parser->parseMessage($data); |
564 | 608 | |
565 | $header = $response->header; | |
566 | $this->assertSame(0x5dd8, $header->get('id')); | |
567 | $this->assertSame(1, $header->get('qdCount')); | |
568 | $this->assertSame(1, $header->get('anCount')); | |
569 | $this->assertSame(0, $header->get('nsCount')); | |
570 | $this->assertSame(0, $header->get('arCount')); | |
571 | $this->assertSame(1, $header->get('qr')); | |
572 | $this->assertSame(Message::OPCODE_QUERY, $header->get('opcode')); | |
573 | $this->assertSame(0, $header->get('aa')); | |
574 | $this->assertSame(0, $header->get('tc')); | |
575 | $this->assertSame(1, $header->get('rd')); | |
576 | $this->assertSame(1, $header->get('ra')); | |
577 | $this->assertSame(0, $header->get('z')); | |
578 | $this->assertSame(Message::RCODE_OK, $header->get('rcode')); | |
609 | $this->assertSame(0x5dd8, $response->id); | |
610 | $this->assertSame(true, $response->qr); | |
611 | $this->assertSame(Message::OPCODE_QUERY, $response->opcode); | |
612 | $this->assertSame(false, $response->aa); | |
613 | $this->assertSame(false, $response->tc); | |
614 | $this->assertSame(true, $response->rd); | |
615 | $this->assertSame(true, $response->ra); | |
616 | $this->assertSame(Message::RCODE_OK, $response->rcode); | |
579 | 617 | |
580 | 618 | $this->assertCount(1, $response->questions); |
581 | $this->assertSame('4.4.8.8.in-addr.arpa', $response->questions[0]['name']); | |
582 | $this->assertSame(Message::TYPE_PTR, $response->questions[0]['type']); | |
583 | $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); | |
619 | $this->assertSame('4.4.8.8.in-addr.arpa', $response->questions[0]->name); | |
620 | $this->assertSame(Message::TYPE_PTR, $response->questions[0]->type); | |
621 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
584 | 622 | |
585 | 623 | $this->assertCount(1, $response->answers); |
586 | 624 | $this->assertSame('4.4.8.8.in-addr.arpa', $response->answers[0]->name); |
588 | 626 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); |
589 | 627 | $this->assertSame(86399, $response->answers[0]->ttl); |
590 | 628 | $this->assertSame('google-public-dns-b.google.com', $response->answers[0]->data); |
629 | } | |
630 | ||
631 | public function testParsePTRResponseWithSpecialCharactersEscaped() | |
632 | { | |
633 | $data = ""; | |
634 | $data .= "5d d8 81 80 00 01 00 01 00 00 00 00"; // header | |
635 | $data .= "08 5f 70 72 69 6e 74 65 72 04 5f 74 63 70 06 64 6e 73 2d 73 64 03 6f 72 67 00"; // question: _printer._tcp.dns-sd.org | |
636 | $data .= "00 0c 00 01"; // question: type PTR, class IN | |
637 | $data .= "c0 0c"; // answer: offset pointer to rdata | |
638 | $data .= "00 0c 00 01"; // answer: type PTR, class IN | |
639 | $data .= "00 01 51 7f"; // answer: ttl 86399 | |
640 | $data .= "00 17"; // answer: rdlength 23 | |
641 | $data .= "14 33 72 64 2e 20 46 6c 6f 6f 72 20 43 6f 70 79 20 52 6f 6f 6d"; // answer: rdata "3rd. Floor Copy Room" … | |
642 | $data .= "c0 0c"; // answer: offset pointer to rdata | |
643 | ||
644 | $data = $this->convertTcpDumpToBinary($data); | |
645 | ||
646 | $response = $this->parser->parseMessage($data); | |
647 | ||
648 | $this->assertCount(1, $response->questions); | |
649 | $this->assertSame('_printer._tcp.dns-sd.org', $response->questions[0]->name); | |
650 | $this->assertSame(Message::TYPE_PTR, $response->questions[0]->type); | |
651 | $this->assertSame(Message::CLASS_IN, $response->questions[0]->class); | |
652 | ||
653 | $this->assertCount(1, $response->answers); | |
654 | $this->assertSame('_printer._tcp.dns-sd.org', $response->answers[0]->name); | |
655 | $this->assertSame(Message::TYPE_PTR, $response->answers[0]->type); | |
656 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
657 | $this->assertSame(86399, $response->answers[0]->ttl); | |
658 | $this->assertSame('3rd\.\ Floor\ Copy\ Room._printer._tcp.dns-sd.org', $response->answers[0]->data); | |
591 | 659 | } |
592 | 660 | |
593 | 661 | /** |
699 | 767 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io |
700 | 768 | $data .= "00 01 00 01"; // question: type A, class IN |
701 | 769 | $data .= "c0 0c"; // answer: offset pointer to igor.io |
770 | ||
771 | $data = $this->convertTcpDumpToBinary($data); | |
772 | ||
773 | $this->parser->parseMessage($data); | |
774 | } | |
775 | ||
776 | /** | |
777 | * @expectedException InvalidArgumentException | |
778 | */ | |
779 | public function testParseMessageResponseWithIncompleteAuthorityRecordThrows() | |
780 | { | |
781 | $data = ""; | |
782 | $data .= "72 62 81 80 00 01 00 00 00 01 00 00"; // header | |
783 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
784 | $data .= "00 01 00 01"; // question: type A, class IN | |
785 | $data .= "c0 0c"; // authority: offset pointer to igor.io | |
786 | ||
787 | $data = $this->convertTcpDumpToBinary($data); | |
788 | ||
789 | $this->parser->parseMessage($data); | |
790 | } | |
791 | ||
792 | /** | |
793 | * @expectedException InvalidArgumentException | |
794 | */ | |
795 | public function testParseMessageResponseWithIncompleteAdditionalRecordThrows() | |
796 | { | |
797 | $data = ""; | |
798 | $data .= "72 62 81 80 00 01 00 00 00 00 00 01"; // header | |
799 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
800 | $data .= "00 01 00 01"; // question: type A, class IN | |
801 | $data .= "c0 0c"; // additional: offset pointer to igor.io | |
702 | 802 | |
703 | 803 | $data = $this->convertTcpDumpToBinary($data); |
704 | 804 | |
724 | 824 | $this->parser->parseMessage($data); |
725 | 825 | } |
726 | 826 | |
827 | /** | |
828 | * @expectedException InvalidArgumentException | |
829 | */ | |
727 | 830 | public function testParseInvalidNSResponseWhereDomainNameIsMissing() |
728 | 831 | { |
729 | 832 | $data = ""; |
732 | 835 | $data .= "00 01 51 80"; // answer: ttl 86400 |
733 | 836 | $data .= "00 00"; // answer: rdlength 0 |
734 | 837 | |
735 | $data = $this->convertTcpDumpToBinary($data); | |
736 | ||
737 | $response = new Message(); | |
738 | $response->header->set('anCount', 1); | |
739 | $response->data = $data; | |
740 | ||
741 | $this->parser->parseAnswer($response); | |
742 | ||
743 | $this->assertCount(0, $response->answers); | |
744 | } | |
745 | ||
838 | $this->parseAnswer($data); | |
839 | } | |
840 | ||
841 | /** | |
842 | * @expectedException InvalidArgumentException | |
843 | */ | |
746 | 844 | public function testParseInvalidAResponseWhereIPIsMissing() |
747 | 845 | { |
748 | 846 | $data = ""; |
751 | 849 | $data .= "00 01 51 80"; // answer: ttl 86400 |
752 | 850 | $data .= "00 00"; // answer: rdlength 0 |
753 | 851 | |
754 | $data = $this->convertTcpDumpToBinary($data); | |
755 | ||
756 | $response = new Message(); | |
757 | $response->header->set('anCount', 1); | |
758 | $response->data = $data; | |
759 | ||
760 | $this->parser->parseAnswer($response); | |
761 | ||
762 | $this->assertCount(0, $response->answers); | |
763 | } | |
764 | ||
852 | $this->parseAnswer($data); | |
853 | } | |
854 | ||
855 | /** | |
856 | * @expectedException InvalidArgumentException | |
857 | */ | |
765 | 858 | public function testParseInvalidAAAAResponseWhereIPIsMissing() |
766 | 859 | { |
767 | 860 | $data = ""; |
770 | 863 | $data .= "00 01 51 80"; // answer: ttl 86400 |
771 | 864 | $data .= "00 00"; // answer: rdlength 0 |
772 | 865 | |
773 | $data = $this->convertTcpDumpToBinary($data); | |
774 | ||
775 | $response = new Message(); | |
776 | $response->header->set('anCount', 1); | |
777 | $response->data = $data; | |
778 | ||
779 | $this->parser->parseAnswer($response); | |
780 | ||
781 | $this->assertCount(0, $response->answers); | |
782 | } | |
783 | ||
866 | $this->parseAnswer($data); | |
867 | } | |
868 | ||
869 | /** | |
870 | * @expectedException InvalidArgumentException | |
871 | */ | |
784 | 872 | public function testParseInvalidTXTResponseWhereTxtChunkExceedsLimit() |
785 | 873 | { |
786 | 874 | $data = ""; |
790 | 878 | $data .= "00 06"; // answer: rdlength 6 |
791 | 879 | $data .= "06 68 65 6c 6c 6f 6f"; // answer: rdata length 6: helloo |
792 | 880 | |
793 | $data = $this->convertTcpDumpToBinary($data); | |
794 | ||
795 | $response = new Message(); | |
796 | $response->header->set('anCount', 1); | |
797 | $response->data = $data; | |
798 | ||
799 | $this->parser->parseAnswer($response); | |
800 | ||
801 | $this->assertCount(0, $response->answers); | |
802 | } | |
803 | ||
881 | $this->parseAnswer($data); | |
882 | } | |
883 | ||
884 | /** | |
885 | * @expectedException InvalidArgumentException | |
886 | */ | |
804 | 887 | public function testParseInvalidMXResponseWhereDomainNameIsIncomplete() |
805 | 888 | { |
806 | 889 | $data = ""; |
810 | 893 | $data .= "00 08"; // answer: rdlength 8 |
811 | 894 | $data .= "00 0a 05 68 65 6c 6c 6f"; // answer: rdata priority 10: hello (missing label end) |
812 | 895 | |
813 | $data = $this->convertTcpDumpToBinary($data); | |
814 | ||
815 | $response = new Message(); | |
816 | $response->header->set('anCount', 1); | |
817 | $response->data = $data; | |
818 | ||
819 | $this->parser->parseAnswer($response); | |
820 | ||
821 | $this->assertCount(0, $response->answers); | |
822 | } | |
823 | ||
896 | $this->parseAnswer($data); | |
897 | } | |
898 | ||
899 | /** | |
900 | * @expectedException InvalidArgumentException | |
901 | */ | |
824 | 902 | public function testParseInvalidMXResponseWhereDomainNameIsMissing() |
825 | 903 | { |
826 | 904 | $data = ""; |
830 | 908 | $data .= "00 02"; // answer: rdlength 2 |
831 | 909 | $data .= "00 0a"; // answer: rdata priority 10 |
832 | 910 | |
833 | $data = $this->convertTcpDumpToBinary($data); | |
834 | ||
835 | $response = new Message(); | |
836 | $response->header->set('anCount', 1); | |
837 | $response->data = $data; | |
838 | ||
839 | $this->parser->parseAnswer($response); | |
840 | ||
841 | $this->assertCount(0, $response->answers); | |
842 | } | |
843 | ||
911 | $this->parseAnswer($data); | |
912 | } | |
913 | ||
914 | /** | |
915 | * @expectedException InvalidArgumentException | |
916 | */ | |
844 | 917 | public function testParseInvalidSRVResponseWhereDomainNameIsIncomplete() |
845 | 918 | { |
846 | 919 | $data = ""; |
850 | 923 | $data .= "00 0b"; // answer: rdlength 11 |
851 | 924 | $data .= "00 0a 00 14 1F 90 04 74 65 73 74"; // answer: rdata priority 10, weight 20, port 8080 test (missing label end) |
852 | 925 | |
853 | $data = $this->convertTcpDumpToBinary($data); | |
854 | ||
855 | $response = new Message(); | |
856 | $response->header->set('anCount', 1); | |
857 | $response->data = $data; | |
858 | ||
859 | $this->parser->parseAnswer($response); | |
860 | ||
861 | $this->assertCount(0, $response->answers); | |
862 | } | |
863 | ||
926 | $this->parseAnswer($data); | |
927 | } | |
928 | ||
929 | /** | |
930 | * @expectedException InvalidArgumentException | |
931 | */ | |
864 | 932 | public function testParseInvalidSRVResponseWhereDomainNameIsMissing() |
865 | 933 | { |
866 | 934 | $data = ""; |
870 | 938 | $data .= "00 06"; // answer: rdlength 6 |
871 | 939 | $data .= "00 0a 00 14 1F 90"; // answer: rdata priority 10, weight 20, port 8080 |
872 | 940 | |
873 | $data = $this->convertTcpDumpToBinary($data); | |
874 | ||
875 | $response = new Message(); | |
876 | $response->header->set('anCount', 1); | |
877 | $response->data = $data; | |
878 | ||
879 | $this->parser->parseAnswer($response); | |
880 | ||
881 | $this->assertCount(0, $response->answers); | |
882 | } | |
883 | ||
941 | $this->parseAnswer($data); | |
942 | } | |
943 | ||
944 | /** | |
945 | * @expectedException InvalidArgumentException | |
946 | */ | |
947 | public function testParseInvalidSSHFPResponseWhereRecordIsTooSmall() | |
948 | { | |
949 | $data = ""; | |
950 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
951 | $data .= "00 2c 00 01"; // answer: type SSHFP, class IN | |
952 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
953 | $data .= "00 02"; // answer: rdlength 2 | |
954 | $data .= "01 01"; // answer: algorithm 1 (RSA), type 1 (SHA), missing fingerprint | |
955 | ||
956 | $this->parseAnswer($data); | |
957 | } | |
958 | ||
959 | /** | |
960 | * @expectedException InvalidArgumentException | |
961 | */ | |
884 | 962 | public function testParseInvalidSOAResponseWhereFlagsAreMissing() |
885 | 963 | { |
886 | 964 | $data = ""; |
891 | 969 | $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) |
892 | 970 | $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) |
893 | 971 | |
894 | $data = $this->convertTcpDumpToBinary($data); | |
895 | ||
896 | $response = new Message(); | |
897 | $response->header->set('anCount', 1); | |
898 | $response->data = $data; | |
899 | ||
900 | $this->parser->parseAnswer($response); | |
901 | ||
902 | $this->assertCount(0, $response->answers); | |
972 | $this->parseAnswer($data); | |
973 | } | |
974 | ||
975 | /** | |
976 | * @expectedException InvalidArgumentException | |
977 | */ | |
978 | public function testParseInvalidCAAResponseEmtpyData() | |
979 | { | |
980 | $data = ""; | |
981 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
982 | $data .= "01 01 00 01"; // answer: type CAA, class IN | |
983 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
984 | $data .= "00 00"; // answer: rdlength 0 | |
985 | ||
986 | $this->parseAnswer($data); | |
987 | } | |
988 | ||
989 | /** | |
990 | * @expectedException InvalidArgumentException | |
991 | */ | |
992 | public function testParseInvalidCAAResponseMissingValue() | |
993 | { | |
994 | $data = ""; | |
995 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
996 | $data .= "01 01 00 01"; // answer: type CAA, class IN | |
997 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
998 | $data .= "00 07"; // answer: rdlength 22 | |
999 | $data .= "00 05 69 73 73 75 65"; // answer: rdata 0, issue | |
1000 | ||
1001 | $this->parseAnswer($data); | |
1002 | } | |
1003 | ||
1004 | /** | |
1005 | * @expectedException InvalidArgumentException | |
1006 | */ | |
1007 | public function testParseInvalidCAAResponseIncompleteTag() | |
1008 | { | |
1009 | $data = ""; | |
1010 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
1011 | $data .= "01 01 00 01"; // answer: type CAA, class IN | |
1012 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
1013 | $data .= "00 0c"; // answer: rdlength 22 | |
1014 | $data .= "00 ff 69 73 73 75 65"; // answer: rdata 0, issue (incomplete due to invalid tag length) | |
1015 | $data .= "68 65 6c 6c 6f"; // answer: hello | |
1016 | ||
1017 | $this->parseAnswer($data); | |
903 | 1018 | } |
904 | 1019 | |
905 | 1020 | private function convertTcpDumpToBinary($input) |
908 | 1023 | |
909 | 1024 | return pack('H*', str_replace(' ', '', $input)); |
910 | 1025 | } |
1026 | ||
1027 | private function parseAnswer($answerData) | |
1028 | { | |
1029 | $data = "72 62 81 80 00 00 00 01 00 00 00 00"; // header with one answer only | |
1030 | $data .= $answerData; | |
1031 | ||
1032 | $data = $this->convertTcpDumpToBinary($data); | |
1033 | ||
1034 | return $this->parser->parseMessage($data); | |
1035 | } | |
911 | 1036 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use React\Tests\Dns\TestCase; | |
5 | use React\Dns\Query\CachedExecutor; | |
6 | use React\Dns\Query\Query; | |
7 | use React\Dns\Model\Message; | |
8 | use React\Dns\Model\Record; | |
9 | use React\Promise; | |
10 | ||
11 | class CachedExecutorTest extends TestCase | |
12 | { | |
13 | /** | |
14 | * @covers React\Dns\Query\CachedExecutor | |
15 | * @test | |
16 | */ | |
17 | public function queryShouldDelegateToDecoratedExecutor() | |
18 | { | |
19 | $executor = $this->createExecutorMock(); | |
20 | $executor | |
21 | ->expects($this->once()) | |
22 | ->method('query') | |
23 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
24 | ->will($this->returnValue($this->createPromiseMock())); | |
25 | ||
26 | $cache = $this->getMockBuilder('React\Dns\Query\RecordCache') | |
27 | ->disableOriginalConstructor() | |
28 | ->getMock(); | |
29 | $cache | |
30 | ->expects($this->once()) | |
31 | ->method('lookup') | |
32 | ->will($this->returnValue(Promise\reject())); | |
33 | $cachedExecutor = new CachedExecutor($executor, $cache); | |
34 | ||
35 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
36 | $cachedExecutor->query('8.8.8.8', $query); | |
37 | } | |
38 | ||
39 | /** | |
40 | * @covers React\Dns\Query\CachedExecutor | |
41 | * @test | |
42 | */ | |
43 | public function callingQueryTwiceShouldUseCachedResult() | |
44 | { | |
45 | $cachedRecords = array(new Record('igor.io', Message::TYPE_A, Message::CLASS_IN)); | |
46 | ||
47 | $executor = $this->createExecutorMock(); | |
48 | $executor | |
49 | ->expects($this->once()) | |
50 | ->method('query') | |
51 | ->will($this->callQueryCallbackWithAddress('178.79.169.131')); | |
52 | ||
53 | $cache = $this->getMockBuilder('React\Dns\Query\RecordCache') | |
54 | ->disableOriginalConstructor() | |
55 | ->getMock(); | |
56 | $cache | |
57 | ->expects($this->at(0)) | |
58 | ->method('lookup') | |
59 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
60 | ->will($this->returnValue(Promise\reject())); | |
61 | $cache | |
62 | ->expects($this->at(1)) | |
63 | ->method('storeResponseMessage') | |
64 | ->with($this->isType('integer'), $this->isInstanceOf('React\Dns\Model\Message')); | |
65 | $cache | |
66 | ->expects($this->at(2)) | |
67 | ->method('lookup') | |
68 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
69 | ->will($this->returnValue(Promise\resolve($cachedRecords))); | |
70 | ||
71 | $cachedExecutor = new CachedExecutor($executor, $cache); | |
72 | ||
73 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
74 | $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {}); | |
75 | $cachedExecutor->query('8.8.8.8', $query, function () {}, function () {}); | |
76 | } | |
77 | ||
78 | private function callQueryCallbackWithAddress($address) | |
79 | { | |
80 | return $this->returnCallback(function ($nameserver, $query) use ($address) { | |
81 | $response = new Message(); | |
82 | $response->header->set('qr', 1); | |
83 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
84 | $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, $address); | |
85 | ||
86 | return Promise\resolve($response); | |
87 | }); | |
88 | } | |
89 | ||
90 | private function createExecutorMock() | |
91 | { | |
92 | return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
93 | } | |
94 | ||
95 | private function createPromiseMock() | |
96 | { | |
97 | return $this->getMockBuilder('React\Promise\PromiseInterface')->getMock(); | |
98 | } | |
99 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Query\CachingExecutor; | |
6 | use React\Dns\Query\Query; | |
7 | use React\Promise\Promise; | |
8 | use React\Tests\Dns\TestCase; | |
9 | use React\Promise\Deferred; | |
10 | use React\Dns\Model\Record; | |
11 | ||
12 | class CachingExecutorTest extends TestCase | |
13 | { | |
14 | public function testQueryWillReturnPendingPromiseWhenCacheIsPendingWithoutSendingQueryToFallbackExecutor() | |
15 | { | |
16 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
17 | $fallback->expects($this->never())->method('query'); | |
18 | ||
19 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
20 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(new Promise(function () { })); | |
21 | ||
22 | $executor = new CachingExecutor($fallback, $cache); | |
23 | ||
24 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
25 | ||
26 | $promise = $executor->query($query); | |
27 | ||
28 | $promise->then($this->expectCallableNever(), $this->expectCallableNever()); | |
29 | } | |
30 | ||
31 | public function testQueryWillReturnPendingPromiseWhenCacheReturnsMissAndWillSendSameQueryToFallbackExecutor() | |
32 | { | |
33 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
34 | ||
35 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
36 | $fallback->expects($this->once())->method('query')->with($query)->willReturn(new Promise(function () { })); | |
37 | ||
38 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
39 | $cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); | |
40 | ||
41 | $executor = new CachingExecutor($fallback, $cache); | |
42 | ||
43 | $promise = $executor->query($query); | |
44 | ||
45 | $promise->then($this->expectCallableNever(), $this->expectCallableNever()); | |
46 | } | |
47 | ||
48 | public function testQueryWillReturnResolvedPromiseWhenCacheReturnsHitWithoutSendingQueryToFallbackExecutor() | |
49 | { | |
50 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
51 | $fallback->expects($this->never())->method('query'); | |
52 | ||
53 | $message = new Message(); | |
54 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
55 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve($message)); | |
56 | ||
57 | $executor = new CachingExecutor($fallback, $cache); | |
58 | ||
59 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
60 | ||
61 | $promise = $executor->query($query); | |
62 | ||
63 | $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); | |
64 | } | |
65 | ||
66 | public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCacheWithMinimumTtlFromRecord() | |
67 | { | |
68 | $message = new Message(); | |
69 | $message->answers[] = new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3700, '127.0.0.1'); | |
70 | $message->answers[] = new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3600, '127.0.0.1'); | |
71 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
72 | $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); | |
73 | ||
74 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
75 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null)); | |
76 | $cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 3600); | |
77 | ||
78 | $executor = new CachingExecutor($fallback, $cache); | |
79 | ||
80 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
81 | ||
82 | $promise = $executor->query($query); | |
83 | ||
84 | $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); | |
85 | } | |
86 | ||
87 | public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesAndSaveMessageToCacheWithDefaultTtl() | |
88 | { | |
89 | $message = new Message(); | |
90 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
91 | $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); | |
92 | ||
93 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
94 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null)); | |
95 | $cache->expects($this->once())->method('set')->with('reactphp.org:1:1', $message, 60); | |
96 | ||
97 | $executor = new CachingExecutor($fallback, $cache); | |
98 | ||
99 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
100 | ||
101 | $promise = $executor->query($query); | |
102 | ||
103 | $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); | |
104 | } | |
105 | ||
106 | public function testQueryWillReturnResolvedPromiseWhenCacheReturnsMissAndFallbackExecutorResolvesWithTruncatedResponseButShouldNotSaveTruncatedMessageToCache() | |
107 | { | |
108 | $message = new Message(); | |
109 | $message->tc = true; | |
110 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
111 | $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); | |
112 | ||
113 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
114 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn(\React\Promise\resolve(null)); | |
115 | $cache->expects($this->never())->method('set'); | |
116 | ||
117 | $executor = new CachingExecutor($fallback, $cache); | |
118 | ||
119 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
120 | ||
121 | $promise = $executor->query($query); | |
122 | ||
123 | $promise->then($this->expectCallableOnceWith($message), $this->expectCallableNever()); | |
124 | } | |
125 | ||
126 | public function testQueryWillReturnRejectedPromiseWhenCacheReturnsMissAndFallbackExecutorRejects() | |
127 | { | |
128 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
129 | ||
130 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
131 | $fallback->expects($this->once())->method('query')->willReturn(\React\Promise\reject(new \RuntimeException())); | |
132 | ||
133 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
134 | $cache->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); | |
135 | ||
136 | $executor = new CachingExecutor($fallback, $cache); | |
137 | ||
138 | $promise = $executor->query($query); | |
139 | ||
140 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); | |
141 | } | |
142 | ||
143 | public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromCache() | |
144 | { | |
145 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
146 | $fallback->expects($this->never())->method('query'); | |
147 | ||
148 | $pending = new Promise(function () { }, $this->expectCallableOnce()); | |
149 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
150 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($pending); | |
151 | ||
152 | $executor = new CachingExecutor($fallback, $cache); | |
153 | ||
154 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
155 | ||
156 | $promise = $executor->query($query); | |
157 | $promise->cancel(); | |
158 | ||
159 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); | |
160 | } | |
161 | ||
162 | public function testCancelQueryWillReturnRejectedPromiseAndCancelPendingPromiseFromFallbackExecutorWhenCacheReturnsMiss() | |
163 | { | |
164 | $pending = new Promise(function () { }, $this->expectCallableOnce()); | |
165 | $fallback = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
166 | $fallback->expects($this->once())->method('query')->willReturn($pending); | |
167 | ||
168 | $deferred = new Deferred(); | |
169 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
170 | $cache->expects($this->once())->method('get')->with('reactphp.org:1:1')->willReturn($deferred->promise()); | |
171 | ||
172 | $executor = new CachingExecutor($fallback, $cache); | |
173 | ||
174 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
175 | ||
176 | $promise = $executor->query($query); | |
177 | $deferred->resolve(null); | |
178 | $promise->cancel(); | |
179 | ||
180 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($this->isInstanceOf('RuntimeException'))); | |
181 | } | |
182 | } |
0 | <?php | |
1 | ||
2 | use React\Dns\Query\CoopExecutor; | |
3 | use React\Dns\Model\Message; | |
4 | use React\Dns\Query\Query; | |
5 | use React\Promise\Promise; | |
6 | use React\Tests\Dns\TestCase; | |
7 | use React\Promise\Deferred; | |
8 | ||
9 | class CoopExecutorTest extends TestCase | |
10 | { | |
11 | public function testQueryOnceWillPassExactQueryToBaseExecutor() | |
12 | { | |
13 | $pending = new Promise(function () { }); | |
14 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
15 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
16 | $base->expects($this->once())->method('query')->with($query)->willReturn($pending); | |
17 | $connector = new CoopExecutor($base); | |
18 | ||
19 | $connector->query($query); | |
20 | } | |
21 | ||
22 | public function testQueryOnceWillResolveWhenBaseExecutorResolves() | |
23 | { | |
24 | $message = new Message(); | |
25 | ||
26 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
27 | $base->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); | |
28 | $connector = new CoopExecutor($base); | |
29 | ||
30 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
31 | $promise = $connector->query($query); | |
32 | ||
33 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
34 | ||
35 | $promise->then($this->expectCallableOnceWith($message)); | |
36 | } | |
37 | ||
38 | public function testQueryOnceWillRejectWhenBaseExecutorRejects() | |
39 | { | |
40 | $exception = new RuntimeException(); | |
41 | ||
42 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
43 | $base->expects($this->once())->method('query')->willReturn(\React\Promise\reject($exception)); | |
44 | $connector = new CoopExecutor($base); | |
45 | ||
46 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
47 | $promise = $connector->query($query); | |
48 | ||
49 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
50 | ||
51 | $promise->then(null, $this->expectCallableOnceWith($exception)); | |
52 | } | |
53 | ||
54 | public function testQueryTwoDifferentQueriesWillPassExactQueryToBaseExecutorTwice() | |
55 | { | |
56 | $pending = new Promise(function () { }); | |
57 | $query1 = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
58 | $query2 = new Query('reactphp.org', Message::TYPE_AAAA, Message::CLASS_IN); | |
59 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
60 | $base->expects($this->exactly(2))->method('query')->withConsecutive( | |
61 | array($query1), | |
62 | array($query2) | |
63 | )->willReturn($pending); | |
64 | $connector = new CoopExecutor($base); | |
65 | ||
66 | $connector->query($query1); | |
67 | $connector->query($query2); | |
68 | } | |
69 | ||
70 | public function testQueryTwiceWillPassExactQueryToBaseExecutorOnceWhenQueryIsStillPending() | |
71 | { | |
72 | $pending = new Promise(function () { }); | |
73 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
74 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
75 | $base->expects($this->once())->method('query')->with($query)->willReturn($pending); | |
76 | $connector = new CoopExecutor($base); | |
77 | ||
78 | $connector->query($query); | |
79 | $connector->query($query); | |
80 | } | |
81 | ||
82 | public function testQueryTwiceWillPassExactQueryToBaseExecutorTwiceWhenFirstQueryIsAlreadyResolved() | |
83 | { | |
84 | $deferred = new Deferred(); | |
85 | $pending = new Promise(function () { }); | |
86 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
87 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
88 | $base->expects($this->exactly(2))->method('query')->with($query)->willReturnOnConsecutiveCalls($deferred->promise(), $pending); | |
89 | ||
90 | $connector = new CoopExecutor($base); | |
91 | ||
92 | $connector->query($query); | |
93 | ||
94 | $deferred->resolve(new Message()); | |
95 | ||
96 | $connector->query($query); | |
97 | } | |
98 | ||
99 | public function testQueryTwiceWillPassExactQueryToBaseExecutorTwiceWhenFirstQueryIsAlreadyRejected() | |
100 | { | |
101 | $deferred = new Deferred(); | |
102 | $pending = new Promise(function () { }); | |
103 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
104 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
105 | $base->expects($this->exactly(2))->method('query')->with($query)->willReturnOnConsecutiveCalls($deferred->promise(), $pending); | |
106 | ||
107 | $connector = new CoopExecutor($base); | |
108 | ||
109 | $connector->query($query); | |
110 | ||
111 | $deferred->reject(new RuntimeException()); | |
112 | ||
113 | $connector->query($query); | |
114 | } | |
115 | ||
116 | public function testCancelQueryWillCancelPromiseFromBaseExecutorAndReject() | |
117 | { | |
118 | $promise = new Promise(function () { }, $this->expectCallableOnce()); | |
119 | ||
120 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
121 | $base->expects($this->once())->method('query')->willReturn($promise); | |
122 | $connector = new CoopExecutor($base); | |
123 | ||
124 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
125 | $promise = $connector->query($query); | |
126 | ||
127 | $promise->cancel(); | |
128 | ||
129 | $promise->then(null, $this->expectCallableOnce()); | |
130 | } | |
131 | ||
132 | public function testCancelOneQueryWhenOtherQueryIsStillPendingWillNotCancelPromiseFromBaseExecutorAndRejectCancelled() | |
133 | { | |
134 | $promise = new Promise(function () { }, $this->expectCallableNever()); | |
135 | ||
136 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
137 | $base->expects($this->once())->method('query')->willReturn($promise); | |
138 | $connector = new CoopExecutor($base); | |
139 | ||
140 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
141 | $promise1 = $connector->query($query); | |
142 | $promise2 = $connector->query($query); | |
143 | ||
144 | $promise1->cancel(); | |
145 | ||
146 | $promise1->then(null, $this->expectCallableOnce()); | |
147 | $promise2->then(null, $this->expectCallableNever()); | |
148 | } | |
149 | ||
150 | public function testCancelSecondQueryWhenFirstQueryIsStillPendingWillNotCancelPromiseFromBaseExecutorAndRejectCancelled() | |
151 | { | |
152 | $promise = new Promise(function () { }, $this->expectCallableNever()); | |
153 | ||
154 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
155 | $base->expects($this->once())->method('query')->willReturn($promise); | |
156 | $connector = new CoopExecutor($base); | |
157 | ||
158 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
159 | $promise1 = $connector->query($query); | |
160 | $promise2 = $connector->query($query); | |
161 | ||
162 | $promise2->cancel(); | |
163 | ||
164 | $promise2->then(null, $this->expectCallableOnce()); | |
165 | $promise1->then(null, $this->expectCallableNever()); | |
166 | } | |
167 | ||
168 | public function testCancelAllPendingQueriesWillCancelPromiseFromBaseExecutorAndRejectCancelled() | |
169 | { | |
170 | $promise = new Promise(function () { }, $this->expectCallableOnce()); | |
171 | ||
172 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
173 | $base->expects($this->once())->method('query')->willReturn($promise); | |
174 | $connector = new CoopExecutor($base); | |
175 | ||
176 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
177 | $promise1 = $connector->query($query); | |
178 | $promise2 = $connector->query($query); | |
179 | ||
180 | $promise1->cancel(); | |
181 | $promise2->cancel(); | |
182 | ||
183 | $promise1->then(null, $this->expectCallableOnce()); | |
184 | $promise2->then(null, $this->expectCallableOnce()); | |
185 | } | |
186 | ||
187 | public function testQueryTwiceWillQueryBaseExecutorTwiceIfFirstQueryHasAlreadyBeenCancelledWhenSecondIsStarted() | |
188 | { | |
189 | $promise = new Promise(function () { }, $this->expectCallableOnce()); | |
190 | $pending = new Promise(function () { }); | |
191 | ||
192 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
193 | $base->expects($this->exactly(2))->method('query')->willReturnOnConsecutiveCalls($promise, $pending); | |
194 | $connector = new CoopExecutor($base); | |
195 | ||
196 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
197 | ||
198 | $promise1 = $connector->query($query); | |
199 | $promise1->cancel(); | |
200 | ||
201 | $promise2 = $connector->query($query); | |
202 | ||
203 | $promise1->then(null, $this->expectCallableOnce()); | |
204 | ||
205 | $promise2->then(null, $this->expectCallableNever()); | |
206 | } | |
207 | ||
208 | public function testCancelQueryShouldNotCauseGarbageReferences() | |
209 | { | |
210 | if (class_exists('React\Promise\When')) { | |
211 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
212 | } | |
213 | ||
214 | $deferred = new Deferred(function () { | |
215 | throw new \RuntimeException(); | |
216 | }); | |
217 | ||
218 | $base = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
219 | $base->expects($this->once())->method('query')->willReturn($deferred->promise()); | |
220 | $connector = new CoopExecutor($base); | |
221 | ||
222 | gc_collect_cycles(); | |
223 | ||
224 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
225 | ||
226 | $promise = $connector->query($query); | |
227 | $promise->cancel(); | |
228 | $promise = null; | |
229 | ||
230 | $this->assertEquals(0, gc_collect_cycles()); | |
231 | } | |
232 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use Clue\React\Block; | |
5 | use React\Dns\Query\Executor; | |
6 | use React\Dns\Query\Query; | |
7 | use React\Dns\Model\Message; | |
8 | use React\Dns\Model\Record; | |
9 | use React\Dns\Protocol\BinaryDumper; | |
10 | use React\Tests\Dns\TestCase; | |
11 | ||
12 | class ExecutorTest extends TestCase | |
13 | { | |
14 | private $loop; | |
15 | private $parser; | |
16 | private $dumper; | |
17 | private $executor; | |
18 | ||
19 | public function setUp() | |
20 | { | |
21 | $this->loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
22 | $this->parser = $this->getMockBuilder('React\Dns\Protocol\Parser')->getMock(); | |
23 | $this->dumper = new BinaryDumper(); | |
24 | ||
25 | $this->executor = new Executor($this->loop, $this->parser, $this->dumper); | |
26 | } | |
27 | ||
28 | /** @test */ | |
29 | public function queryShouldCreateUdpRequest() | |
30 | { | |
31 | $timer = $this->createTimerMock(); | |
32 | $this->loop | |
33 | ->expects($this->any()) | |
34 | ->method('addTimer') | |
35 | ->will($this->returnValue($timer)); | |
36 | ||
37 | $this->executor = $this->createExecutorMock(); | |
38 | $this->executor | |
39 | ->expects($this->once()) | |
40 | ->method('createConnection') | |
41 | ->with('8.8.8.8:53', 'udp') | |
42 | ->will($this->returnNewConnectionMock(false)); | |
43 | ||
44 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
45 | $this->executor->query('8.8.8.8:53', $query); | |
46 | } | |
47 | ||
48 | /** @test */ | |
49 | public function resolveShouldRejectIfRequestIsLargerThan512Bytes() | |
50 | { | |
51 | $query = new Query(str_repeat('a', 512).'.igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
52 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
53 | ||
54 | $this->setExpectedException('RuntimeException', 'DNS query for ' . $query->name . ' failed: Requested transport "tcp" not available, only UDP is supported in this version'); | |
55 | Block\await($promise, $this->loop); | |
56 | } | |
57 | ||
58 | /** @test */ | |
59 | public function resolveShouldCloseConnectionWhenCancelled() | |
60 | { | |
61 | $conn = $this->createConnectionMock(false); | |
62 | $conn->expects($this->once())->method('close'); | |
63 | ||
64 | $timer = $this->createTimerMock(); | |
65 | $this->loop | |
66 | ->expects($this->any()) | |
67 | ->method('addTimer') | |
68 | ->will($this->returnValue($timer)); | |
69 | ||
70 | $this->executor = $this->createExecutorMock(); | |
71 | $this->executor | |
72 | ->expects($this->once()) | |
73 | ->method('createConnection') | |
74 | ->with('8.8.8.8:53', 'udp') | |
75 | ->will($this->returnValue($conn)); | |
76 | ||
77 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
78 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
79 | ||
80 | $promise->cancel(); | |
81 | ||
82 | $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled'); | |
83 | Block\await($promise, $this->loop); | |
84 | } | |
85 | ||
86 | /** @test */ | |
87 | public function resolveShouldNotStartOrCancelTimerWhenCancelledWithTimeoutIsNull() | |
88 | { | |
89 | $this->loop | |
90 | ->expects($this->never()) | |
91 | ->method('addTimer'); | |
92 | ||
93 | $this->executor = new Executor($this->loop, $this->parser, $this->dumper, null); | |
94 | ||
95 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
96 | $promise = $this->executor->query('127.0.0.1:53', $query); | |
97 | ||
98 | $promise->cancel(); | |
99 | ||
100 | $this->setExpectedException('React\Dns\Query\CancellationException', 'DNS query for igor.io has been cancelled'); | |
101 | Block\await($promise, $this->loop); | |
102 | } | |
103 | ||
104 | /** @test */ | |
105 | public function resolveShouldRejectIfResponseIsTruncated() | |
106 | { | |
107 | $timer = $this->createTimerMock(); | |
108 | ||
109 | $this->loop | |
110 | ->expects($this->any()) | |
111 | ->method('addTimer') | |
112 | ->will($this->returnValue($timer)); | |
113 | ||
114 | $this->parser | |
115 | ->expects($this->once()) | |
116 | ->method('parseMessage') | |
117 | ->will($this->returnTruncatedResponse()); | |
118 | ||
119 | $this->executor = $this->createExecutorMock(); | |
120 | $this->executor | |
121 | ->expects($this->once()) | |
122 | ->method('createConnection') | |
123 | ->with('8.8.8.8:53', 'udp') | |
124 | ->will($this->returnNewConnectionMock()); | |
125 | ||
126 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
127 | $this->executor->query('8.8.8.8:53', $query); | |
128 | } | |
129 | ||
130 | /** @test */ | |
131 | public function resolveShouldFailIfUdpThrow() | |
132 | { | |
133 | $this->loop | |
134 | ->expects($this->never()) | |
135 | ->method('addTimer'); | |
136 | ||
137 | $this->parser | |
138 | ->expects($this->never()) | |
139 | ->method('parseMessage'); | |
140 | ||
141 | $this->executor = $this->createExecutorMock(); | |
142 | $this->executor | |
143 | ->expects($this->once()) | |
144 | ->method('createConnection') | |
145 | ->with('8.8.8.8:53', 'udp') | |
146 | ->will($this->throwException(new \Exception('Nope'))); | |
147 | ||
148 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
149 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
150 | ||
151 | $this->setExpectedException('RuntimeException', 'DNS query for igor.io failed: Nope'); | |
152 | Block\await($promise, $this->loop); | |
153 | } | |
154 | ||
155 | /** @test */ | |
156 | public function resolveShouldCancelTimerWhenFullResponseIsReceived() | |
157 | { | |
158 | $conn = $this->createConnectionMock(); | |
159 | ||
160 | $this->parser | |
161 | ->expects($this->once()) | |
162 | ->method('parseMessage') | |
163 | ->will($this->returnStandardResponse()); | |
164 | ||
165 | $this->executor = $this->createExecutorMock(); | |
166 | $this->executor | |
167 | ->expects($this->at(0)) | |
168 | ->method('createConnection') | |
169 | ->with('8.8.8.8:53', 'udp') | |
170 | ->will($this->returnNewConnectionMock()); | |
171 | ||
172 | ||
173 | $timer = $this->createTimerMock(); | |
174 | ||
175 | $this->loop | |
176 | ->expects($this->once()) | |
177 | ->method('addTimer') | |
178 | ->with(5, $this->isInstanceOf('Closure')) | |
179 | ->will($this->returnValue($timer)); | |
180 | ||
181 | $this->loop | |
182 | ->expects($this->once()) | |
183 | ->method('cancelTimer') | |
184 | ->with($timer); | |
185 | ||
186 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
187 | $this->executor->query('8.8.8.8:53', $query); | |
188 | } | |
189 | ||
190 | /** @test */ | |
191 | public function resolveShouldCloseConnectionOnTimeout() | |
192 | { | |
193 | $this->executor = $this->createExecutorMock(); | |
194 | $this->executor | |
195 | ->expects($this->at(0)) | |
196 | ->method('createConnection') | |
197 | ->with('8.8.8.8:53', 'udp') | |
198 | ->will($this->returnNewConnectionMock(false)); | |
199 | ||
200 | $timer = $this->createTimerMock(); | |
201 | ||
202 | $this->loop | |
203 | ->expects($this->never()) | |
204 | ->method('cancelTimer'); | |
205 | ||
206 | $this->loop | |
207 | ->expects($this->once()) | |
208 | ->method('addTimer') | |
209 | ->with(5, $this->isInstanceOf('Closure')) | |
210 | ->will($this->returnCallback(function ($time, $callback) use (&$timerCallback, $timer) { | |
211 | $timerCallback = $callback; | |
212 | return $timer; | |
213 | })); | |
214 | ||
215 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
216 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
217 | ||
218 | $this->assertNotNull($timerCallback); | |
219 | $timerCallback(); | |
220 | ||
221 | $this->setExpectedException('React\Dns\Query\TimeoutException', 'DNS query for igor.io timed out'); | |
222 | Block\await($promise, $this->loop); | |
223 | } | |
224 | ||
225 | private function returnStandardResponse() | |
226 | { | |
227 | $that = $this; | |
228 | $callback = function ($data) use ($that) { | |
229 | $response = new Message(); | |
230 | $that->convertMessageToStandardResponse($response); | |
231 | return $response; | |
232 | }; | |
233 | ||
234 | return $this->returnCallback($callback); | |
235 | } | |
236 | ||
237 | private function returnTruncatedResponse() | |
238 | { | |
239 | $that = $this; | |
240 | $callback = function ($data) use ($that) { | |
241 | $response = new Message(); | |
242 | $that->convertMessageToTruncatedResponse($response); | |
243 | return $response; | |
244 | }; | |
245 | ||
246 | return $this->returnCallback($callback); | |
247 | } | |
248 | ||
249 | public function convertMessageToStandardResponse(Message $response) | |
250 | { | |
251 | $response->header->set('qr', 1); | |
252 | $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
253 | $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); | |
254 | $response->prepare(); | |
255 | ||
256 | return $response; | |
257 | } | |
258 | ||
259 | public function convertMessageToTruncatedResponse(Message $response) | |
260 | { | |
261 | $this->convertMessageToStandardResponse($response); | |
262 | $response->header->set('tc', 1); | |
263 | $response->prepare(); | |
264 | ||
265 | return $response; | |
266 | } | |
267 | ||
268 | private function returnNewConnectionMock($emitData = true) | |
269 | { | |
270 | $conn = $this->createConnectionMock($emitData); | |
271 | ||
272 | $callback = function () use ($conn) { | |
273 | return $conn; | |
274 | }; | |
275 | ||
276 | return $this->returnCallback($callback); | |
277 | } | |
278 | ||
279 | private function createConnectionMock($emitData = true) | |
280 | { | |
281 | $conn = $this->getMockBuilder('React\Stream\DuplexStreamInterface')->getMock(); | |
282 | $conn | |
283 | ->expects($this->any()) | |
284 | ->method('on') | |
285 | ->with('data', $this->isInstanceOf('Closure')) | |
286 | ->will($this->returnCallback(function ($name, $callback) use ($emitData) { | |
287 | $emitData && $callback(null); | |
288 | })); | |
289 | ||
290 | return $conn; | |
291 | } | |
292 | ||
293 | private function createTimerMock() | |
294 | { | |
295 | return $this->getMockBuilder( | |
296 | interface_exists('React\EventLoop\TimerInterface') ? 'React\EventLoop\TimerInterface' : 'React\EventLoop\Timer\TimerInterface' | |
297 | )->getMock(); | |
298 | } | |
299 | ||
300 | private function createExecutorMock() | |
301 | { | |
302 | return $this->getMockBuilder('React\Dns\Query\Executor') | |
303 | ->setConstructorArgs(array($this->loop, $this->parser, $this->dumper)) | |
304 | ->setMethods(array('createConnection')) | |
305 | ->getMock(); | |
306 | } | |
307 | } |
24 | 24 | $this->hosts->expects($this->never())->method('getIpsForHost'); |
25 | 25 | $this->fallback->expects($this->once())->method('query'); |
26 | 26 | |
27 | $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_MX, Message::CLASS_IN, 0)); | |
27 | $this->executor->query(new Query('google.com', Message::TYPE_MX, Message::CLASS_IN)); | |
28 | 28 | } |
29 | 29 | |
30 | 30 | public function testFallsBackIfNoIpsWereFound() |
32 | 32 | $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array()); |
33 | 33 | $this->fallback->expects($this->once())->method('query'); |
34 | 34 | |
35 | $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); | |
35 | $this->executor->query(new Query('google.com', Message::TYPE_A, Message::CLASS_IN)); | |
36 | 36 | } |
37 | 37 | |
38 | 38 | public function testReturnsResponseMessageIfIpsWereFound() |
40 | 40 | $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1')); |
41 | 41 | $this->fallback->expects($this->never())->method('query'); |
42 | 42 | |
43 | $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); | |
43 | $ret = $this->executor->query(new Query('google.com', Message::TYPE_A, Message::CLASS_IN)); | |
44 | 44 | } |
45 | 45 | |
46 | 46 | public function testFallsBackIfNoIpv4Matches() |
48 | 48 | $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1')); |
49 | 49 | $this->fallback->expects($this->once())->method('query'); |
50 | 50 | |
51 | $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_A, Message::CLASS_IN, 0)); | |
51 | $ret = $this->executor->query(new Query('google.com', Message::TYPE_A, Message::CLASS_IN)); | |
52 | 52 | } |
53 | 53 | |
54 | 54 | public function testReturnsResponseMessageIfIpv6AddressesWereFound() |
56 | 56 | $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1')); |
57 | 57 | $this->fallback->expects($this->never())->method('query'); |
58 | 58 | |
59 | $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0)); | |
59 | $ret = $this->executor->query(new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN)); | |
60 | 60 | } |
61 | 61 | |
62 | 62 | public function testFallsBackIfNoIpv6Matches() |
64 | 64 | $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1')); |
65 | 65 | $this->fallback->expects($this->once())->method('query'); |
66 | 66 | |
67 | $ret = $this->executor->query('8.8.8.8', new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN, 0)); | |
67 | $ret = $this->executor->query(new Query('google.com', Message::TYPE_AAAA, Message::CLASS_IN)); | |
68 | 68 | } |
69 | 69 | |
70 | 70 | public function testDoesReturnReverseIpv4Lookup() |
72 | 72 | $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array('localhost')); |
73 | 73 | $this->fallback->expects($this->never())->method('query'); |
74 | 74 | |
75 | $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
75 | $this->executor->query(new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN)); | |
76 | 76 | } |
77 | 77 | |
78 | 78 | public function testFallsBackIfNoReverseIpv4Matches() |
80 | 80 | $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array()); |
81 | 81 | $this->fallback->expects($this->once())->method('query'); |
82 | 82 | |
83 | $this->executor->query('8.8.8.8', new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
83 | $this->executor->query(new Query('1.0.0.127.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN)); | |
84 | 84 | } |
85 | 85 | |
86 | 86 | public function testDoesReturnReverseIpv6Lookup() |
88 | 88 | $this->hosts->expects($this->once())->method('getHostsForIp')->with('2a02:2e0:3fe:100::6')->willReturn(array('ip6-localhost')); |
89 | 89 | $this->fallback->expects($this->never())->method('query'); |
90 | 90 | |
91 | $this->executor->query('8.8.8.8', new Query('6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.e.f.3.0.0.e.2.0.2.0.a.2.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
91 | $this->executor->query(new Query('6.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.1.0.e.f.3.0.0.e.2.0.2.0.a.2.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN)); | |
92 | 92 | } |
93 | 93 | |
94 | 94 | public function testFallsBackForInvalidAddress() |
96 | 96 | $this->hosts->expects($this->never())->method('getHostsForIp'); |
97 | 97 | $this->fallback->expects($this->once())->method('query'); |
98 | 98 | |
99 | $this->executor->query('8.8.8.8', new Query('example.com', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
99 | $this->executor->query(new Query('example.com', Message::TYPE_PTR, Message::CLASS_IN)); | |
100 | 100 | } |
101 | 101 | |
102 | 102 | public function testReverseFallsBackForInvalidIpv4Address() |
104 | 104 | $this->hosts->expects($this->never())->method('getHostsForIp'); |
105 | 105 | $this->fallback->expects($this->once())->method('query'); |
106 | 106 | |
107 | $this->executor->query('8.8.8.8', new Query('::1.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
107 | $this->executor->query(new Query('::1.in-addr.arpa', Message::TYPE_PTR, Message::CLASS_IN)); | |
108 | 108 | } |
109 | 109 | |
110 | 110 | public function testReverseFallsBackForInvalidLengthIpv6Address() |
112 | 112 | $this->hosts->expects($this->never())->method('getHostsForIp'); |
113 | 113 | $this->fallback->expects($this->once())->method('query'); |
114 | 114 | |
115 | $this->executor->query('8.8.8.8', new Query('abcd.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
115 | $this->executor->query(new Query('abcd.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN)); | |
116 | 116 | } |
117 | 117 | |
118 | 118 | public function testReverseFallsBackForInvalidHexIpv6Address() |
120 | 120 | $this->hosts->expects($this->never())->method('getHostsForIp'); |
121 | 121 | $this->fallback->expects($this->once())->method('query'); |
122 | 122 | |
123 | $this->executor->query('8.8.8.8', new Query('zZz.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN, 0)); | |
123 | $this->executor->query(new Query('zZz.ip6.arpa', Message::TYPE_PTR, Message::CLASS_IN)); | |
124 | 124 | } |
125 | 125 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use PHPUnit\Framework\TestCase; | |
5 | use React\Dns\Query\RecordBag; | |
6 | use React\Dns\Model\Message; | |
7 | use React\Dns\Model\Record; | |
8 | ||
9 | class RecordBagTest extends TestCase | |
10 | { | |
11 | /** | |
12 | * @covers React\Dns\Query\RecordBag | |
13 | * @test | |
14 | */ | |
15 | public function emptyBagShouldBeEmpty() | |
16 | { | |
17 | $recordBag = new RecordBag(); | |
18 | ||
19 | $this->assertSame(array(), $recordBag->all()); | |
20 | } | |
21 | ||
22 | /** | |
23 | * @covers React\Dns\Query\RecordBag | |
24 | * @test | |
25 | */ | |
26 | public function setShouldSetTheValue() | |
27 | { | |
28 | $currentTime = 1345656451; | |
29 | ||
30 | $recordBag = new RecordBag(); | |
31 | $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600)); | |
32 | ||
33 | $records = $recordBag->all(); | |
34 | $this->assertCount(1, $records); | |
35 | $this->assertSame('igor.io', $records[0]->name); | |
36 | $this->assertSame(Message::TYPE_A, $records[0]->type); | |
37 | $this->assertSame(Message::CLASS_IN, $records[0]->class); | |
38 | } | |
39 | ||
40 | /** | |
41 | * @covers React\Dns\Query\RecordBag | |
42 | * @test | |
43 | */ | |
44 | public function setShouldAcceptMxRecord() | |
45 | { | |
46 | $currentTime = 1345656451; | |
47 | ||
48 | $recordBag = new RecordBag(); | |
49 | $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_MX, Message::CLASS_IN, 3600, array('priority' => 10, 'target' => 'igor.io'))); | |
50 | ||
51 | $records = $recordBag->all(); | |
52 | $this->assertCount(1, $records); | |
53 | $this->assertSame('igor.io', $records[0]->name); | |
54 | $this->assertSame(Message::TYPE_MX, $records[0]->type); | |
55 | $this->assertSame(Message::CLASS_IN, $records[0]->class); | |
56 | $this->assertSame(array('priority' => 10, 'target' => 'igor.io'), $records[0]->data); | |
57 | } | |
58 | ||
59 | /** | |
60 | * @covers React\Dns\Query\RecordBag | |
61 | * @test | |
62 | */ | |
63 | public function setShouldSetManyValues() | |
64 | { | |
65 | $currentTime = 1345656451; | |
66 | ||
67 | $recordBag = new RecordBag(); | |
68 | $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
69 | $recordBag->set($currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132')); | |
70 | ||
71 | $records = $recordBag->all(); | |
72 | $this->assertCount(2, $records); | |
73 | $this->assertSame('igor.io', $records[0]->name); | |
74 | $this->assertSame(Message::TYPE_A, $records[0]->type); | |
75 | $this->assertSame(Message::CLASS_IN, $records[0]->class); | |
76 | $this->assertSame('178.79.169.131', $records[0]->data); | |
77 | $this->assertSame('igor.io', $records[1]->name); | |
78 | $this->assertSame(Message::TYPE_A, $records[1]->type); | |
79 | $this->assertSame(Message::CLASS_IN, $records[1]->class); | |
80 | $this->assertSame('178.79.169.132', $records[1]->data); | |
81 | } | |
82 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use PHPUnit\Framework\TestCase; | |
5 | use React\Cache\ArrayCache; | |
6 | use React\Dns\Model\Message; | |
7 | use React\Dns\Model\Record; | |
8 | use React\Dns\Query\RecordCache; | |
9 | use React\Dns\Query\Query; | |
10 | use React\Promise\PromiseInterface; | |
11 | use React\Promise\Promise; | |
12 | ||
13 | class RecordCacheTest extends TestCase | |
14 | { | |
15 | /** | |
16 | * @covers React\Dns\Query\RecordCache | |
17 | * @test | |
18 | */ | |
19 | public function lookupOnNewCacheMissShouldReturnNull() | |
20 | { | |
21 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
22 | ||
23 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
24 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); | |
25 | ||
26 | $cache = new RecordCache($base); | |
27 | $promise = $cache->lookup($query); | |
28 | ||
29 | $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); | |
30 | } | |
31 | ||
32 | /** | |
33 | * @covers React\Dns\Query\RecordCache | |
34 | * @test | |
35 | */ | |
36 | public function lookupOnLegacyCacheMissShouldReturnNull() | |
37 | { | |
38 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
39 | ||
40 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
41 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\reject()); | |
42 | ||
43 | $cache = new RecordCache($base); | |
44 | $promise = $cache->lookup($query); | |
45 | ||
46 | $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); | |
47 | } | |
48 | ||
49 | /** | |
50 | * @covers React\Dns\Query\RecordCache | |
51 | * @test | |
52 | */ | |
53 | public function storeRecordPendingCacheDoesNotSetCache() | |
54 | { | |
55 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
56 | $pending = new Promise(function () { }); | |
57 | ||
58 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
59 | $base->expects($this->once())->method('get')->willReturn($pending); | |
60 | $base->expects($this->never())->method('set'); | |
61 | ||
62 | $cache = new RecordCache($base); | |
63 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
64 | } | |
65 | ||
66 | /** | |
67 | * @covers React\Dns\Query\RecordCache | |
68 | * @test | |
69 | */ | |
70 | public function storeRecordOnNewCacheMissSetsCache() | |
71 | { | |
72 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
73 | ||
74 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
75 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\resolve(null)); | |
76 | $base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string')); | |
77 | ||
78 | $cache = new RecordCache($base); | |
79 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
80 | } | |
81 | ||
82 | /** | |
83 | * @covers React\Dns\Query\RecordCache | |
84 | * @test | |
85 | */ | |
86 | public function storeRecordOnOldCacheMissSetsCache() | |
87 | { | |
88 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
89 | ||
90 | $base = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
91 | $base->expects($this->once())->method('get')->willReturn(\React\Promise\reject()); | |
92 | $base->expects($this->once())->method('set')->with($this->isType('string'), $this->isType('string')); | |
93 | ||
94 | $cache = new RecordCache($base); | |
95 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
96 | } | |
97 | ||
98 | /** | |
99 | * @covers React\Dns\Query\RecordCache | |
100 | * @test | |
101 | */ | |
102 | public function storeRecordShouldMakeLookupSucceed() | |
103 | { | |
104 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
105 | ||
106 | $cache = new RecordCache(new ArrayCache()); | |
107 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
108 | $promise = $cache->lookup($query); | |
109 | ||
110 | $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); | |
111 | $cachedRecords = $this->getPromiseValue($promise); | |
112 | ||
113 | $this->assertCount(1, $cachedRecords); | |
114 | $this->assertSame('178.79.169.131', $cachedRecords[0]->data); | |
115 | } | |
116 | ||
117 | /** | |
118 | * @covers React\Dns\Query\RecordCache | |
119 | * @test | |
120 | */ | |
121 | public function storeTwoRecordsShouldReturnBoth() | |
122 | { | |
123 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
124 | ||
125 | $cache = new RecordCache(new ArrayCache()); | |
126 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
127 | $cache->storeRecord($query->currentTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132')); | |
128 | $promise = $cache->lookup($query); | |
129 | ||
130 | $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); | |
131 | $cachedRecords = $this->getPromiseValue($promise); | |
132 | ||
133 | $this->assertCount(2, $cachedRecords); | |
134 | $this->assertSame('178.79.169.131', $cachedRecords[0]->data); | |
135 | $this->assertSame('178.79.169.132', $cachedRecords[1]->data); | |
136 | } | |
137 | ||
138 | /** | |
139 | * @covers React\Dns\Query\RecordCache | |
140 | * @test | |
141 | */ | |
142 | public function storeResponseMessageShouldStoreAllAnswerValues() | |
143 | { | |
144 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
145 | ||
146 | $response = new Message(); | |
147 | $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); | |
148 | $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.132'); | |
149 | $response->prepare(); | |
150 | ||
151 | $cache = new RecordCache(new ArrayCache()); | |
152 | $cache->storeResponseMessage($query->currentTime, $response); | |
153 | $promise = $cache->lookup($query); | |
154 | ||
155 | $this->assertInstanceOf('React\Promise\FulfilledPromise', $promise); | |
156 | $cachedRecords = $this->getPromiseValue($promise); | |
157 | ||
158 | $this->assertCount(2, $cachedRecords); | |
159 | $this->assertSame('178.79.169.131', $cachedRecords[0]->data); | |
160 | $this->assertSame('178.79.169.132', $cachedRecords[1]->data); | |
161 | } | |
162 | ||
163 | /** | |
164 | * @covers React\Dns\Query\RecordCache | |
165 | * @test | |
166 | */ | |
167 | public function expireShouldExpireDeadRecords() | |
168 | { | |
169 | $cachedTime = 1345656451; | |
170 | $currentTime = $cachedTime + 3605; | |
171 | ||
172 | $cache = new RecordCache(new ArrayCache()); | |
173 | $cache->storeRecord($cachedTime, new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131')); | |
174 | $cache->expire($currentTime); | |
175 | ||
176 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, $currentTime); | |
177 | $promise = $cache->lookup($query); | |
178 | ||
179 | $this->assertInstanceOf('React\Promise\RejectedPromise', $promise); | |
180 | } | |
181 | ||
182 | private function getPromiseValue(PromiseInterface $promise) | |
183 | { | |
184 | $capturedValue = null; | |
185 | ||
186 | $promise->then(function ($value) use (&$capturedValue) { | |
187 | $capturedValue = $value; | |
188 | }); | |
189 | ||
190 | return $capturedValue; | |
191 | } | |
192 | } |
23 | 23 | $executor |
24 | 24 | ->expects($this->once()) |
25 | 25 | ->method('query') |
26 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
26 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
27 | 27 | ->will($this->returnValue($this->expectPromiseOnce())); |
28 | 28 | |
29 | 29 | $retryExecutor = new RetryExecutor($executor, 2); |
30 | 30 | |
31 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
32 | $retryExecutor->query('8.8.8.8', $query); | |
31 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
32 | $retryExecutor->query($query); | |
33 | 33 | } |
34 | 34 | |
35 | 35 | /** |
44 | 44 | $executor |
45 | 45 | ->expects($this->exactly(2)) |
46 | 46 | ->method('query') |
47 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
47 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
48 | 48 | ->will($this->onConsecutiveCalls( |
49 | $this->returnCallback(function ($domain, $query) { | |
49 | $this->returnCallback(function ($query) { | |
50 | 50 | return Promise\reject(new TimeoutException("timeout")); |
51 | 51 | }), |
52 | $this->returnCallback(function ($domain, $query) use ($response) { | |
52 | $this->returnCallback(function ($query) use ($response) { | |
53 | 53 | return Promise\resolve($response); |
54 | 54 | }) |
55 | 55 | )); |
64 | 64 | |
65 | 65 | $retryExecutor = new RetryExecutor($executor, 2); |
66 | 66 | |
67 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
68 | $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); | |
67 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
68 | $retryExecutor->query($query)->then($callback, $errorback); | |
69 | 69 | } |
70 | 70 | |
71 | 71 | /** |
78 | 78 | $executor |
79 | 79 | ->expects($this->exactly(3)) |
80 | 80 | ->method('query') |
81 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
82 | ->will($this->returnCallback(function ($domain, $query) { | |
81 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
82 | ->will($this->returnCallback(function ($query) { | |
83 | 83 | return Promise\reject(new TimeoutException("timeout")); |
84 | 84 | })); |
85 | 85 | |
93 | 93 | |
94 | 94 | $retryExecutor = new RetryExecutor($executor, 2); |
95 | 95 | |
96 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
97 | $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); | |
96 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
97 | $retryExecutor->query($query)->then($callback, $errorback); | |
98 | 98 | } |
99 | 99 | |
100 | 100 | /** |
107 | 107 | $executor |
108 | 108 | ->expects($this->once()) |
109 | 109 | ->method('query') |
110 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
111 | ->will($this->returnCallback(function ($domain, $query) { | |
110 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
111 | ->will($this->returnCallback(function ($query) { | |
112 | 112 | return Promise\reject(new \Exception); |
113 | 113 | })); |
114 | 114 | |
122 | 122 | |
123 | 123 | $retryExecutor = new RetryExecutor($executor, 2); |
124 | 124 | |
125 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
126 | $retryExecutor->query('8.8.8.8', $query)->then($callback, $errorback); | |
125 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
126 | $retryExecutor->query($query)->then($callback, $errorback); | |
127 | 127 | } |
128 | 128 | |
129 | 129 | /** |
138 | 138 | $executor |
139 | 139 | ->expects($this->once()) |
140 | 140 | ->method('query') |
141 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
142 | ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { | |
141 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
142 | ->will($this->returnCallback(function ($query) use (&$cancelled) { | |
143 | 143 | $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { |
144 | 144 | ++$cancelled; |
145 | 145 | $reject(new CancellationException('Cancelled')); |
151 | 151 | |
152 | 152 | $retryExecutor = new RetryExecutor($executor, 2); |
153 | 153 | |
154 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
155 | $promise = $retryExecutor->query('8.8.8.8', $query); | |
154 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
155 | $promise = $retryExecutor->query($query); | |
156 | 156 | |
157 | 157 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); |
158 | 158 | |
174 | 174 | $executor |
175 | 175 | ->expects($this->exactly(2)) |
176 | 176 | ->method('query') |
177 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
177 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
178 | 178 | ->will($this->onConsecutiveCalls( |
179 | 179 | $this->returnValue($deferred->promise()), |
180 | $this->returnCallback(function ($domain, $query) use (&$cancelled) { | |
180 | $this->returnCallback(function ($query) use (&$cancelled) { | |
181 | 181 | $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { |
182 | 182 | ++$cancelled; |
183 | 183 | $reject(new CancellationException('Cancelled')); |
189 | 189 | |
190 | 190 | $retryExecutor = new RetryExecutor($executor, 2); |
191 | 191 | |
192 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
193 | $promise = $retryExecutor->query('8.8.8.8', $query); | |
192 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
193 | $promise = $retryExecutor->query($query); | |
194 | 194 | |
195 | 195 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); |
196 | 196 | |
216 | 216 | $executor |
217 | 217 | ->expects($this->once()) |
218 | 218 | ->method('query') |
219 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
219 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
220 | 220 | ->willReturn(Promise\resolve($this->createStandardResponse())); |
221 | 221 | |
222 | 222 | $retryExecutor = new RetryExecutor($executor, 0); |
223 | 223 | |
224 | 224 | gc_collect_cycles(); |
225 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
226 | $retryExecutor->query('8.8.8.8', $query); | |
225 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
226 | $retryExecutor->query($query); | |
227 | 227 | |
228 | 228 | $this->assertEquals(0, gc_collect_cycles()); |
229 | 229 | } |
242 | 242 | $executor |
243 | 243 | ->expects($this->any()) |
244 | 244 | ->method('query') |
245 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
245 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
246 | 246 | ->willReturn(Promise\reject(new TimeoutException("timeout"))); |
247 | 247 | |
248 | 248 | $retryExecutor = new RetryExecutor($executor, 0); |
249 | 249 | |
250 | 250 | gc_collect_cycles(); |
251 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
252 | $retryExecutor->query('8.8.8.8', $query); | |
251 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
252 | $retryExecutor->query($query); | |
253 | 253 | |
254 | 254 | $this->assertEquals(0, gc_collect_cycles()); |
255 | 255 | } |
272 | 272 | $executor |
273 | 273 | ->expects($this->once()) |
274 | 274 | ->method('query') |
275 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
275 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
276 | 276 | ->willReturn($deferred->promise()); |
277 | 277 | |
278 | 278 | $retryExecutor = new RetryExecutor($executor, 0); |
279 | 279 | |
280 | 280 | gc_collect_cycles(); |
281 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
282 | $promise = $retryExecutor->query('8.8.8.8', $query); | |
281 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
282 | $promise = $retryExecutor->query($query); | |
283 | 283 | $promise->cancel(); |
284 | 284 | $promise = null; |
285 | 285 | |
300 | 300 | $executor |
301 | 301 | ->expects($this->once()) |
302 | 302 | ->method('query') |
303 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
304 | ->will($this->returnCallback(function ($domain, $query) { | |
303 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
304 | ->will($this->returnCallback(function ($query) { | |
305 | 305 | return Promise\reject(new \Exception); |
306 | 306 | })); |
307 | 307 | |
308 | 308 | $retryExecutor = new RetryExecutor($executor, 2); |
309 | 309 | |
310 | 310 | gc_collect_cycles(); |
311 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
312 | $retryExecutor->query('8.8.8.8', $query); | |
311 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
312 | $retryExecutor->query($query); | |
313 | 313 | |
314 | 314 | $this->assertEquals(0, gc_collect_cycles()); |
315 | 315 | } |
338 | 338 | protected function createStandardResponse() |
339 | 339 | { |
340 | 340 | $response = new Message(); |
341 | $response->header->set('qr', 1); | |
342 | $response->questions[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
341 | $response->qr = true; | |
342 | $response->questions[] = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
343 | 343 | $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'); |
344 | $response->prepare(); | |
345 | 344 | |
346 | 345 | return $response; |
347 | 346 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Query\Query; | |
6 | use React\Dns\Query\SelectiveTransportExecutor; | |
7 | use React\Promise\Deferred; | |
8 | use React\Promise\Promise; | |
9 | use React\Tests\Dns\TestCase; | |
10 | ||
11 | class SelectiveTransportExecutorTest extends TestCase | |
12 | { | |
13 | public function setUp() | |
14 | { | |
15 | $this->datagram = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
16 | $this->stream = $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); | |
17 | ||
18 | $this->executor = new SelectiveTransportExecutor($this->datagram, $this->stream); | |
19 | } | |
20 | ||
21 | public function testQueryResolvesWhenDatagramTransportResolvesWithoutUsingStreamTransport() | |
22 | { | |
23 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
24 | ||
25 | $response = new Message(); | |
26 | ||
27 | $this->datagram | |
28 | ->expects($this->once()) | |
29 | ->method('query') | |
30 | ->with($query) | |
31 | ->willReturn(\React\Promise\resolve($response)); | |
32 | ||
33 | $this->stream | |
34 | ->expects($this->never()) | |
35 | ->method('query'); | |
36 | ||
37 | $promise = $this->executor->query($query); | |
38 | ||
39 | $promise->then($this->expectCallableOnceWith($response)); | |
40 | } | |
41 | ||
42 | public function testQueryResolvesWhenStreamTransportResolvesAfterDatagramTransportRejectsWithSizeError() | |
43 | { | |
44 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
45 | ||
46 | $response = new Message(); | |
47 | ||
48 | $this->datagram | |
49 | ->expects($this->once()) | |
50 | ->method('query') | |
51 | ->with($query) | |
52 | ->willReturn(\React\Promise\reject(new \RuntimeException('', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90))); | |
53 | ||
54 | $this->stream | |
55 | ->expects($this->once()) | |
56 | ->method('query') | |
57 | ->with($query) | |
58 | ->willReturn(\React\Promise\resolve($response)); | |
59 | ||
60 | $promise = $this->executor->query($query); | |
61 | ||
62 | $promise->then($this->expectCallableOnceWith($response)); | |
63 | } | |
64 | ||
65 | public function testQueryRejectsWhenDatagramTransportRejectsWithRuntimeExceptionWithoutUsingStreamTransport() | |
66 | { | |
67 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
68 | ||
69 | $this->datagram | |
70 | ->expects($this->once()) | |
71 | ->method('query') | |
72 | ->with($query) | |
73 | ->willReturn(\React\Promise\reject(new \RuntimeException())); | |
74 | ||
75 | $this->stream | |
76 | ->expects($this->never()) | |
77 | ->method('query'); | |
78 | ||
79 | $promise = $this->executor->query($query); | |
80 | ||
81 | $promise->then(null, $this->expectCallableOnce()); | |
82 | } | |
83 | ||
84 | public function testQueryRejectsWhenStreamTransportRejectsAfterDatagramTransportRejectsWithSizeError() | |
85 | { | |
86 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
87 | ||
88 | $this->datagram | |
89 | ->expects($this->once()) | |
90 | ->method('query') | |
91 | ->with($query) | |
92 | ->willReturn(\React\Promise\reject(new \RuntimeException('', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90))); | |
93 | ||
94 | $this->stream | |
95 | ->expects($this->once()) | |
96 | ->method('query') | |
97 | ->with($query) | |
98 | ->willReturn(\React\Promise\reject(new \RuntimeException())); | |
99 | ||
100 | $promise = $this->executor->query($query); | |
101 | ||
102 | $promise->then(null, $this->expectCallableOnce()); | |
103 | } | |
104 | ||
105 | public function testCancelPromiseWillCancelPromiseFromDatagramExecutor() | |
106 | { | |
107 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
108 | ||
109 | $this->datagram | |
110 | ->expects($this->once()) | |
111 | ->method('query') | |
112 | ->with($query) | |
113 | ->willReturn(new Promise(function () {}, $this->expectCallableOnce())); | |
114 | ||
115 | $promise = $this->executor->query($query); | |
116 | $promise->cancel(); | |
117 | } | |
118 | ||
119 | public function testCancelPromiseWillCancelPromiseFromStreamExecutorWhenDatagramExecutorRejectedWithTruncatedResponse() | |
120 | { | |
121 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
122 | ||
123 | $deferred = new Deferred(); | |
124 | $this->datagram | |
125 | ->expects($this->once()) | |
126 | ->method('query') | |
127 | ->with($query) | |
128 | ->willReturn($deferred->promise()); | |
129 | ||
130 | $this->stream | |
131 | ->expects($this->once()) | |
132 | ->method('query') | |
133 | ->with($query) | |
134 | ->willReturn(new Promise(function () {}, $this->expectCallableOnce())); | |
135 | ||
136 | $promise = $this->executor->query($query); | |
137 | $deferred->reject(new \RuntimeException('', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90)); | |
138 | $promise->cancel(); | |
139 | } | |
140 | ||
141 | public function testCancelPromiseShouldNotCreateAnyGarbageReferences() | |
142 | { | |
143 | if (class_exists('React\Promise\When')) { | |
144 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
145 | } | |
146 | ||
147 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
148 | ||
149 | $this->datagram | |
150 | ->expects($this->once()) | |
151 | ->method('query') | |
152 | ->with($query) | |
153 | ->willReturn(new Promise(function () {}, function () { | |
154 | throw new \RuntimeException('Cancelled'); | |
155 | })); | |
156 | ||
157 | gc_collect_cycles(); | |
158 | $promise = $this->executor->query($query); | |
159 | $promise->cancel(); | |
160 | unset($promise); | |
161 | ||
162 | $this->assertEquals(0, gc_collect_cycles()); | |
163 | } | |
164 | ||
165 | public function testCancelPromiseAfterTruncatedResponseShouldNotCreateAnyGarbageReferences() | |
166 | { | |
167 | if (class_exists('React\Promise\When')) { | |
168 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
169 | } | |
170 | ||
171 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
172 | ||
173 | $deferred = new Deferred(); | |
174 | $this->datagram | |
175 | ->expects($this->once()) | |
176 | ->method('query') | |
177 | ->with($query) | |
178 | ->willReturn($deferred->promise()); | |
179 | ||
180 | $this->stream | |
181 | ->expects($this->once()) | |
182 | ->method('query') | |
183 | ->with($query) | |
184 | ->willReturn(new Promise(function () {}, function () { | |
185 | throw new \RuntimeException('Cancelled'); | |
186 | })); | |
187 | ||
188 | gc_collect_cycles(); | |
189 | $promise = $this->executor->query($query); | |
190 | $deferred->reject(new \RuntimeException('', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90)); | |
191 | $promise->cancel(); | |
192 | unset($promise); | |
193 | ||
194 | $this->assertEquals(0, gc_collect_cycles()); | |
195 | } | |
196 | ||
197 | public function testRejectedPromiseAfterTruncatedResponseShouldNotCreateAnyGarbageReferences() | |
198 | { | |
199 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
200 | ||
201 | $this->datagram | |
202 | ->expects($this->once()) | |
203 | ->method('query') | |
204 | ->with($query) | |
205 | ->willReturn(\React\Promise\reject(new \RuntimeException('', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90))); | |
206 | ||
207 | $this->stream | |
208 | ->expects($this->once()) | |
209 | ->method('query') | |
210 | ->with($query) | |
211 | ->willReturn(\React\Promise\reject(new \RuntimeException())); | |
212 | ||
213 | gc_collect_cycles(); | |
214 | $promise = $this->executor->query($query); | |
215 | unset($promise); | |
216 | ||
217 | $this->assertEquals(0, gc_collect_cycles()); | |
218 | } | |
219 | } |
0 | <?php | |
1 | ||
2 | namespace React\Tests\Dns\Query; | |
3 | ||
4 | use React\Dns\Model\Message; | |
5 | use React\Dns\Protocol\BinaryDumper; | |
6 | use React\Dns\Protocol\Parser; | |
7 | use React\Dns\Query\Query; | |
8 | use React\Dns\Query\TcpTransportExecutor; | |
9 | use React\EventLoop\Factory; | |
10 | use React\Tests\Dns\TestCase; | |
11 | ||
12 | class TcpTransportExecutorTest extends TestCase | |
13 | { | |
14 | /** | |
15 | * @dataProvider provideDefaultPortProvider | |
16 | * @param string $input | |
17 | * @param string $expected | |
18 | */ | |
19 | public function testCtorShouldAcceptNameserverAddresses($input, $expected) | |
20 | { | |
21 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
22 | ||
23 | $executor = new TcpTransportExecutor($input, $loop); | |
24 | ||
25 | $ref = new \ReflectionProperty($executor, 'nameserver'); | |
26 | $ref->setAccessible(true); | |
27 | $value = $ref->getValue($executor); | |
28 | ||
29 | $this->assertEquals($expected, $value); | |
30 | } | |
31 | ||
32 | public static function provideDefaultPortProvider() | |
33 | { | |
34 | return array( | |
35 | array( | |
36 | '8.8.8.8', | |
37 | '8.8.8.8:53' | |
38 | ), | |
39 | array( | |
40 | '1.2.3.4:5', | |
41 | '1.2.3.4:5' | |
42 | ), | |
43 | array( | |
44 | 'tcp://1.2.3.4', | |
45 | '1.2.3.4:53' | |
46 | ), | |
47 | array( | |
48 | 'tcp://1.2.3.4:53', | |
49 | '1.2.3.4:53' | |
50 | ), | |
51 | array( | |
52 | '::1', | |
53 | '[::1]:53' | |
54 | ), | |
55 | array( | |
56 | '[::1]:53', | |
57 | '[::1]:53' | |
58 | ) | |
59 | ); | |
60 | } | |
61 | ||
62 | /** | |
63 | * @expectedException InvalidArgumentException | |
64 | */ | |
65 | public function testCtorShouldThrowWhenNameserverAddressIsInvalid() | |
66 | { | |
67 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
68 | ||
69 | new TcpTransportExecutor('///', $loop); | |
70 | } | |
71 | ||
72 | /** | |
73 | * @expectedException InvalidArgumentException | |
74 | */ | |
75 | public function testCtorShouldThrowWhenNameserverAddressContainsHostname() | |
76 | { | |
77 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
78 | ||
79 | new TcpTransportExecutor('localhost', $loop); | |
80 | } | |
81 | ||
82 | /** | |
83 | * @expectedException InvalidArgumentException | |
84 | */ | |
85 | public function testCtorShouldThrowWhenNameserverSchemeIsInvalid() | |
86 | { | |
87 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
88 | ||
89 | new TcpTransportExecutor('udp://1.2.3.4', $loop); | |
90 | } | |
91 | ||
92 | public function testQueryRejectsIfMessageExceedsMaximumMessageSize() | |
93 | { | |
94 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
95 | $loop->expects($this->never())->method('addWriteStream'); | |
96 | ||
97 | $executor = new TcpTransportExecutor('8.8.8.8:53', $loop); | |
98 | ||
99 | $query = new Query('google.' . str_repeat('.com', 60000), Message::TYPE_A, Message::CLASS_IN); | |
100 | $promise = $executor->query($query); | |
101 | ||
102 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
103 | $promise->then(null, $this->expectCallableOnce()); | |
104 | } | |
105 | ||
106 | public function testQueryRejectsIfServerConnectionFails() | |
107 | { | |
108 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
109 | $loop->expects($this->never())->method('addWriteStream'); | |
110 | ||
111 | $executor = new TcpTransportExecutor('::1', $loop); | |
112 | ||
113 | $ref = new \ReflectionProperty($executor, 'nameserver'); | |
114 | $ref->setAccessible(true); | |
115 | $ref->setValue($executor, '///'); | |
116 | ||
117 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
118 | $promise = $executor->query($query); | |
119 | ||
120 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
121 | $promise->then(null, $this->expectCallableOnce()); | |
122 | } | |
123 | ||
124 | public function testQueryRejectsOnCancellationWithoutClosingSocketButStartsIdleTimer() | |
125 | { | |
126 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
127 | $loop->expects($this->once())->method('addWriteStream'); | |
128 | $loop->expects($this->never())->method('removeWriteStream'); | |
129 | $loop->expects($this->never())->method('addReadStream'); | |
130 | $loop->expects($this->never())->method('removeReadStream'); | |
131 | ||
132 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
133 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); | |
134 | $loop->expects($this->never())->method('cancelTimer'); | |
135 | ||
136 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
137 | $address = stream_socket_get_name($server, false); | |
138 | ||
139 | $executor = new TcpTransportExecutor($address, $loop); | |
140 | ||
141 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
142 | $promise = $executor->query($query); | |
143 | $promise->cancel(); | |
144 | ||
145 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
146 | $promise->then(null, $this->expectCallableOnce()); | |
147 | } | |
148 | ||
149 | public function testTriggerIdleTimerAfterQueryRejectedOnCancellationWillCloseSocket() | |
150 | { | |
151 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
152 | $loop->expects($this->once())->method('addWriteStream'); | |
153 | $loop->expects($this->once())->method('removeWriteStream'); | |
154 | $loop->expects($this->never())->method('addReadStream'); | |
155 | $loop->expects($this->never())->method('removeReadStream'); | |
156 | ||
157 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
158 | $timerCallback = null; | |
159 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timerCallback) { | |
160 | $timerCallback = $cb; | |
161 | return true; | |
162 | }))->willReturn($timer); | |
163 | $loop->expects($this->once())->method('cancelTimer')->with($timer); | |
164 | ||
165 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
166 | $address = stream_socket_get_name($server, false); | |
167 | ||
168 | $executor = new TcpTransportExecutor($address, $loop); | |
169 | ||
170 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
171 | $promise = $executor->query($query); | |
172 | $promise->cancel(); | |
173 | ||
174 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
175 | $promise->then(null, $this->expectCallableOnce()); | |
176 | ||
177 | // trigger idle timer | |
178 | $this->assertNotNull($timerCallback); | |
179 | $timerCallback(); | |
180 | } | |
181 | ||
182 | public function testQueryRejectsOnCancellationWithoutClosingSocketAndWithoutStartingIdleTimerWhenOtherQueryIsStillPending() | |
183 | { | |
184 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
185 | $loop->expects($this->once())->method('addWriteStream'); | |
186 | $loop->expects($this->never())->method('removeWriteStream'); | |
187 | $loop->expects($this->never())->method('addReadStream'); | |
188 | $loop->expects($this->never())->method('removeReadStream'); | |
189 | ||
190 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
191 | $loop->expects($this->never())->method('addTimer'); | |
192 | $loop->expects($this->never())->method('cancelTimer'); | |
193 | ||
194 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
195 | $address = stream_socket_get_name($server, false); | |
196 | ||
197 | $executor = new TcpTransportExecutor($address, $loop); | |
198 | ||
199 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
200 | $promise1 = $executor->query($query); | |
201 | $promise2 = $executor->query($query); | |
202 | $promise2->cancel(); | |
203 | ||
204 | $promise1->then($this->expectCallableNever(), $this->expectCallableNever()); | |
205 | $promise2->then(null, $this->expectCallableOnce()); | |
206 | } | |
207 | ||
208 | public function testQueryAgainAfterPreviousWasCancelledReusesExistingSocket() | |
209 | { | |
210 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
211 | $loop->expects($this->once())->method('addWriteStream'); | |
212 | $loop->expects($this->never())->method('removeWriteStream'); | |
213 | $loop->expects($this->never())->method('addReadStream'); | |
214 | $loop->expects($this->never())->method('removeReadStream'); | |
215 | ||
216 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
217 | $address = stream_socket_get_name($server, false); | |
218 | ||
219 | $executor = new TcpTransportExecutor($address, $loop); | |
220 | ||
221 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
222 | $promise = $executor->query($query); | |
223 | $promise->cancel(); | |
224 | ||
225 | $executor->query($query); | |
226 | } | |
227 | ||
228 | public function testQueryRejectsWhenServerIsNotListening() | |
229 | { | |
230 | $loop = Factory::create(); | |
231 | ||
232 | $executor = new TcpTransportExecutor('127.0.0.1:1', $loop); | |
233 | ||
234 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
235 | ||
236 | $wait = true; | |
237 | $executor->query($query)->then( | |
238 | null, | |
239 | function ($e) use (&$wait) { | |
240 | $wait = false; | |
241 | throw $e; | |
242 | } | |
243 | ); | |
244 | ||
245 | \Clue\React\Block\sleep(0.01, $loop); | |
246 | if ($wait) { | |
247 | \Clue\React\Block\sleep(0.2, $loop); | |
248 | } | |
249 | ||
250 | $this->assertFalse($wait); | |
251 | } | |
252 | ||
253 | public function testQueryStaysPendingWhenClientCanNotSendExcessiveMessageInOneChunkWhenServerClosesSocket() | |
254 | { | |
255 | $writableCallback = null; | |
256 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
257 | $loop->expects($this->once())->method('addWriteStream')->with($this->anything(), $this->callback(function ($cb) use (&$writableCallback) { | |
258 | $writableCallback = $cb; | |
259 | return true; | |
260 | })); | |
261 | $loop->expects($this->once())->method('addReadStream'); | |
262 | $loop->expects($this->never())->method('removeWriteStream'); | |
263 | $loop->expects($this->never())->method('removeReadStream'); | |
264 | ||
265 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
266 | ||
267 | $address = stream_socket_get_name($server, false); | |
268 | $executor = new TcpTransportExecutor($address, $loop); | |
269 | ||
270 | $query = new Query('google' . str_repeat('.com', 10000), Message::TYPE_A, Message::CLASS_IN); | |
271 | ||
272 | $promise = $executor->query($query); | |
273 | ||
274 | $client = stream_socket_accept($server); | |
275 | fclose($client); | |
276 | ||
277 | $executor->handleWritable(); | |
278 | ||
279 | $promise->then($this->expectCallableNever(), $this->expectCallableNever()); | |
280 | ||
281 | $ref = new \ReflectionProperty($executor, 'writePending'); | |
282 | $ref->setAccessible(true); | |
283 | $writePending = $ref->getValue($executor); | |
284 | ||
285 | $this->assertTrue($writePending); | |
286 | } | |
287 | ||
288 | public function testQueryRejectsWhenServerClosesConnection() | |
289 | { | |
290 | $loop = Factory::create(); | |
291 | ||
292 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
293 | $loop->addReadStream($server, function ($server) use ($loop) { | |
294 | $client = stream_socket_accept($server); | |
295 | fclose($client); | |
296 | }); | |
297 | ||
298 | $address = stream_socket_get_name($server, false); | |
299 | $executor = new TcpTransportExecutor($address, $loop); | |
300 | ||
301 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
302 | ||
303 | $wait = true; | |
304 | $executor->query($query)->then( | |
305 | null, | |
306 | function ($e) use (&$wait) { | |
307 | $wait = false; | |
308 | throw $e; | |
309 | } | |
310 | ); | |
311 | ||
312 | \Clue\React\Block\sleep(0.01, $loop); | |
313 | if ($wait) { | |
314 | \Clue\React\Block\sleep(0.2, $loop); | |
315 | } | |
316 | ||
317 | $this->assertFalse($wait); | |
318 | } | |
319 | ||
320 | public function testQueryKeepsPendingIfServerSendsIncompleteMessageLength() | |
321 | { | |
322 | $loop = Factory::create(); | |
323 | ||
324 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
325 | $loop->addReadStream($server, function ($server) use ($loop) { | |
326 | $client = stream_socket_accept($server); | |
327 | $loop->addReadStream($client, function ($client) use ($loop) { | |
328 | $loop->removeReadStream($client); | |
329 | fwrite($client, "\x00"); | |
330 | }); | |
331 | ||
332 | // keep reference to client to avoid disconnecting | |
333 | $loop->addTimer(1, function () use ($client) { }); | |
334 | }); | |
335 | ||
336 | $address = stream_socket_get_name($server, false); | |
337 | $executor = new TcpTransportExecutor($address, $loop); | |
338 | ||
339 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
340 | ||
341 | $wait = true; | |
342 | $executor->query($query)->then( | |
343 | null, | |
344 | function ($e) use (&$wait) { | |
345 | $wait = false; | |
346 | throw $e; | |
347 | } | |
348 | ); | |
349 | ||
350 | \Clue\React\Block\sleep(0.2, $loop); | |
351 | $this->assertTrue($wait); | |
352 | } | |
353 | ||
354 | public function testQueryKeepsPendingIfServerSendsIncompleteMessageBody() | |
355 | { | |
356 | $loop = Factory::create(); | |
357 | ||
358 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
359 | $loop->addReadStream($server, function ($server) use ($loop) { | |
360 | $client = stream_socket_accept($server); | |
361 | $loop->addReadStream($client, function ($client) use ($loop) { | |
362 | $loop->removeReadStream($client); | |
363 | fwrite($client, "\x00\xff" . "some incomplete message data"); | |
364 | }); | |
365 | ||
366 | // keep reference to client to avoid disconnecting | |
367 | $loop->addTimer(1, function () use ($client) { }); | |
368 | }); | |
369 | ||
370 | $address = stream_socket_get_name($server, false); | |
371 | $executor = new TcpTransportExecutor($address, $loop); | |
372 | ||
373 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
374 | ||
375 | $wait = true; | |
376 | $executor->query($query)->then( | |
377 | null, | |
378 | function ($e) use (&$wait) { | |
379 | $wait = false; | |
380 | throw $e; | |
381 | } | |
382 | ); | |
383 | ||
384 | \Clue\React\Block\sleep(0.2, $loop); | |
385 | $this->assertTrue($wait); | |
386 | } | |
387 | ||
388 | public function testQueryRejectsWhenServerSendsInvalidMessage() | |
389 | { | |
390 | $loop = Factory::create(); | |
391 | ||
392 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
393 | $loop->addReadStream($server, function ($server) use ($loop) { | |
394 | $client = stream_socket_accept($server); | |
395 | $loop->addReadStream($client, function ($client) use ($loop) { | |
396 | $loop->removeReadStream($client); | |
397 | fwrite($client, "\x00\x0f" . 'invalid message'); | |
398 | }); | |
399 | }); | |
400 | ||
401 | $address = stream_socket_get_name($server, false); | |
402 | $executor = new TcpTransportExecutor($address, $loop); | |
403 | ||
404 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
405 | ||
406 | $wait = true; | |
407 | $executor->query($query)->then( | |
408 | null, | |
409 | function ($e) use (&$wait) { | |
410 | $wait = false; | |
411 | throw $e; | |
412 | } | |
413 | ); | |
414 | ||
415 | \Clue\React\Block\sleep(0.01, $loop); | |
416 | if ($wait) { | |
417 | \Clue\React\Block\sleep(0.2, $loop); | |
418 | } | |
419 | ||
420 | $this->assertFalse($wait); | |
421 | } | |
422 | ||
423 | public function testQueryRejectsWhenServerSendsInvalidId() | |
424 | { | |
425 | $parser = new Parser(); | |
426 | $dumper = new BinaryDumper(); | |
427 | ||
428 | $loop = Factory::create(); | |
429 | ||
430 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
431 | $loop->addReadStream($server, function ($server) use ($loop, $parser, $dumper) { | |
432 | $client = stream_socket_accept($server); | |
433 | $loop->addReadStream($client, function ($client) use ($loop, $parser, $dumper) { | |
434 | $loop->removeReadStream($client); | |
435 | $data = fread($client, 512); | |
436 | ||
437 | list(, $length) = unpack('n', substr($data, 0, 2)); | |
438 | assert(strlen($data) - 2 === $length); | |
439 | $data = substr($data, 2); | |
440 | ||
441 | $message = $parser->parseMessage($data); | |
442 | $message->id = 0; | |
443 | ||
444 | $data = $dumper->toBinary($message); | |
445 | $data = pack('n', strlen($data)) . $data; | |
446 | ||
447 | fwrite($client, $data); | |
448 | }); | |
449 | }); | |
450 | ||
451 | $address = stream_socket_get_name($server, false); | |
452 | $executor = new TcpTransportExecutor($address, $loop); | |
453 | ||
454 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
455 | ||
456 | $wait = true; | |
457 | $executor->query($query)->then( | |
458 | null, | |
459 | function ($e) use (&$wait) { | |
460 | $wait = false; | |
461 | throw $e; | |
462 | } | |
463 | ); | |
464 | ||
465 | \Clue\React\Block\sleep(0.01, $loop); | |
466 | if ($wait) { | |
467 | \Clue\React\Block\sleep(0.2, $loop); | |
468 | } | |
469 | ||
470 | $this->assertFalse($wait); | |
471 | } | |
472 | ||
473 | public function testQueryRejectsIfServerSendsTruncatedResponse() | |
474 | { | |
475 | $parser = new Parser(); | |
476 | $dumper = new BinaryDumper(); | |
477 | ||
478 | $loop = Factory::create(); | |
479 | ||
480 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
481 | $loop->addReadStream($server, function ($server) use ($loop, $parser, $dumper) { | |
482 | $client = stream_socket_accept($server); | |
483 | $loop->addReadStream($client, function ($client) use ($loop, $parser, $dumper) { | |
484 | $loop->removeReadStream($client); | |
485 | $data = fread($client, 512); | |
486 | ||
487 | list(, $length) = unpack('n', substr($data, 0, 2)); | |
488 | assert(strlen($data) - 2 === $length); | |
489 | $data = substr($data, 2); | |
490 | ||
491 | $message = $parser->parseMessage($data); | |
492 | $message->tc = true; | |
493 | ||
494 | $data = $dumper->toBinary($message); | |
495 | $data = pack('n', strlen($data)) . $data; | |
496 | ||
497 | fwrite($client, $data); | |
498 | }); | |
499 | }); | |
500 | ||
501 | $address = stream_socket_get_name($server, false); | |
502 | $executor = new TcpTransportExecutor($address, $loop); | |
503 | ||
504 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
505 | ||
506 | $wait = true; | |
507 | $executor->query($query)->then( | |
508 | null, | |
509 | function ($e) use (&$wait) { | |
510 | $wait = false; | |
511 | throw $e; | |
512 | } | |
513 | ); | |
514 | ||
515 | \Clue\React\Block\sleep(0.01, $loop); | |
516 | if ($wait) { | |
517 | \Clue\React\Block\sleep(0.2, $loop); | |
518 | } | |
519 | ||
520 | $this->assertFalse($wait); | |
521 | } | |
522 | ||
523 | public function testQueryResolvesIfServerSendsValidResponse() | |
524 | { | |
525 | $loop = Factory::create(); | |
526 | ||
527 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
528 | $loop->addReadStream($server, function ($server) use ($loop) { | |
529 | $client = stream_socket_accept($server); | |
530 | $loop->addReadStream($client, function ($client) use ($loop) { | |
531 | $loop->removeReadStream($client); | |
532 | $data = fread($client, 512); | |
533 | ||
534 | list(, $length) = unpack('n', substr($data, 0, 2)); | |
535 | assert(strlen($data) - 2 === $length); | |
536 | ||
537 | fwrite($client, $data); | |
538 | }); | |
539 | }); | |
540 | ||
541 | $address = stream_socket_get_name($server, false); | |
542 | $executor = new TcpTransportExecutor($address, $loop); | |
543 | ||
544 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
545 | ||
546 | $promise = $executor->query($query); | |
547 | $response = \Clue\React\Block\await($promise, $loop, 0.2); | |
548 | ||
549 | $this->assertInstanceOf('React\Dns\Model\Message', $response); | |
550 | } | |
551 | ||
552 | public function testQueryRejectsIfSocketIsClosedAfterPreviousQueryThatWasStillPending() | |
553 | { | |
554 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
555 | $loop->expects($this->exactly(2))->method('addWriteStream'); | |
556 | $loop->expects($this->exactly(2))->method('removeWriteStream'); | |
557 | $loop->expects($this->once())->method('addReadStream'); | |
558 | $loop->expects($this->once())->method('removeReadStream'); | |
559 | ||
560 | $loop->expects($this->never())->method('addTimer'); | |
561 | $loop->expects($this->never())->method('cancelTimer'); | |
562 | ||
563 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
564 | $address = stream_socket_get_name($server, false); | |
565 | $executor = new TcpTransportExecutor($address, $loop); | |
566 | ||
567 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
568 | ||
569 | $promise1 = $executor->query($query); | |
570 | ||
571 | $client = stream_socket_accept($server); | |
572 | ||
573 | $executor->handleWritable(); | |
574 | ||
575 | // manually close socket before processing second write | |
576 | $ref = new \ReflectionProperty($executor, 'socket'); | |
577 | $ref->setAccessible(true); | |
578 | $socket = $ref->getValue($executor); | |
579 | fclose($socket); | |
580 | fclose($client); | |
581 | ||
582 | $promise2 = $executor->query($query); | |
583 | ||
584 | $executor->handleWritable(); | |
585 | ||
586 | $promise1->then(null, $this->expectCallableOnce()); | |
587 | $promise2->then(null, $this->expectCallableOnce()); | |
588 | } | |
589 | ||
590 | public function testQueryResolvesIfServerSendsBackResponseMessageAndWillStartIdleTimer() | |
591 | { | |
592 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
593 | $loop->expects($this->once())->method('addWriteStream'); | |
594 | $loop->expects($this->once())->method('removeWriteStream'); | |
595 | $loop->expects($this->once())->method('addReadStream'); | |
596 | $loop->expects($this->never())->method('removeReadStream'); | |
597 | ||
598 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); | |
599 | $loop->expects($this->never())->method('cancelTimer'); | |
600 | ||
601 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
602 | $address = stream_socket_get_name($server, false); | |
603 | $executor = new TcpTransportExecutor($address, $loop); | |
604 | ||
605 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
606 | ||
607 | $promise = $executor->query($query); | |
608 | ||
609 | // use outgoing buffer as response message | |
610 | $ref = new \ReflectionProperty($executor, 'writeBuffer'); | |
611 | $ref->setAccessible(true); | |
612 | $data = $ref->getValue($executor); | |
613 | ||
614 | $client = stream_socket_accept($server); | |
615 | fwrite($client, $data); | |
616 | ||
617 | $executor->handleWritable(); | |
618 | $executor->handleRead(); | |
619 | ||
620 | $promise->then($this->expectCallableOnce()); | |
621 | } | |
622 | ||
623 | public function testQueryResolvesIfServerSendsBackResponseMessageAfterCancellingQueryAndWillStartIdleTimer() | |
624 | { | |
625 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
626 | $loop->expects($this->once())->method('addWriteStream'); | |
627 | $loop->expects($this->once())->method('removeWriteStream'); | |
628 | $loop->expects($this->once())->method('addReadStream'); | |
629 | $loop->expects($this->never())->method('removeReadStream'); | |
630 | ||
631 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
632 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); | |
633 | $loop->expects($this->never())->method('cancelTimer'); | |
634 | ||
635 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
636 | $address = stream_socket_get_name($server, false); | |
637 | $executor = new TcpTransportExecutor($address, $loop); | |
638 | ||
639 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
640 | ||
641 | $promise = $executor->query($query); | |
642 | $promise->cancel(); | |
643 | ||
644 | // use outgoing buffer as response message | |
645 | $ref = new \ReflectionProperty($executor, 'writeBuffer'); | |
646 | $ref->setAccessible(true); | |
647 | $data = $ref->getValue($executor); | |
648 | ||
649 | $client = stream_socket_accept($server); | |
650 | fwrite($client, $data); | |
651 | ||
652 | $executor->handleWritable(); | |
653 | $executor->handleRead(); | |
654 | ||
655 | //$promise->then(null, $this->expectCallableOnce()); | |
656 | } | |
657 | ||
658 | public function testQueryResolvesIfServerSendsBackResponseMessageAfterCancellingOtherQueryAndWillStartIdleTimer() | |
659 | { | |
660 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
661 | $loop->expects($this->once())->method('addWriteStream'); | |
662 | $loop->expects($this->once())->method('removeWriteStream'); | |
663 | $loop->expects($this->once())->method('addReadStream'); | |
664 | $loop->expects($this->never())->method('removeReadStream'); | |
665 | ||
666 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything()); | |
667 | $loop->expects($this->never())->method('cancelTimer'); | |
668 | ||
669 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
670 | $address = stream_socket_get_name($server, false); | |
671 | $executor = new TcpTransportExecutor($address, $loop); | |
672 | ||
673 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
674 | ||
675 | $promise = $executor->query($query); | |
676 | ||
677 | // use outgoing buffer as response message | |
678 | $ref = new \ReflectionProperty($executor, 'writeBuffer'); | |
679 | $ref->setAccessible(true); | |
680 | $data = $ref->getValue($executor); | |
681 | ||
682 | $client = stream_socket_accept($server); | |
683 | fwrite($client, $data); | |
684 | ||
685 | $another = $executor->query($query); | |
686 | $another->cancel(); | |
687 | ||
688 | $executor->handleWritable(); | |
689 | $executor->handleRead(); | |
690 | ||
691 | $promise->then($this->expectCallableOnce()); | |
692 | } | |
693 | ||
694 | public function testTriggerIdleTimerAfterPreviousQueryResolvedWillCloseIdleSocketConnection() | |
695 | { | |
696 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
697 | $loop->expects($this->once())->method('addWriteStream'); | |
698 | $loop->expects($this->once())->method('removeWriteStream'); | |
699 | $loop->expects($this->once())->method('addReadStream'); | |
700 | $loop->expects($this->once())->method('removeReadStream'); | |
701 | ||
702 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
703 | $timerCallback = null; | |
704 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->callback(function ($cb) use (&$timerCallback) { | |
705 | $timerCallback = $cb; | |
706 | return true; | |
707 | }))->willReturn($timer); | |
708 | $loop->expects($this->once())->method('cancelTimer')->with($timer); | |
709 | ||
710 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
711 | $address = stream_socket_get_name($server, false); | |
712 | $executor = new TcpTransportExecutor($address, $loop); | |
713 | ||
714 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
715 | ||
716 | $promise = $executor->query($query); | |
717 | ||
718 | // use outgoing buffer as response message | |
719 | $ref = new \ReflectionProperty($executor, 'writeBuffer'); | |
720 | $ref->setAccessible(true); | |
721 | $data = $ref->getValue($executor); | |
722 | ||
723 | $client = stream_socket_accept($server); | |
724 | fwrite($client, $data); | |
725 | ||
726 | $executor->handleWritable(); | |
727 | $executor->handleRead(); | |
728 | ||
729 | $promise->then($this->expectCallableOnce()); | |
730 | ||
731 | // trigger idle timer | |
732 | $this->assertNotNull($timerCallback); | |
733 | $timerCallback(); | |
734 | } | |
735 | ||
736 | public function testClosingConnectionAfterPreviousQueryResolvedWillCancelIdleTimer() | |
737 | { | |
738 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
739 | $loop->expects($this->once())->method('addWriteStream'); | |
740 | $loop->expects($this->once())->method('removeWriteStream'); | |
741 | $loop->expects($this->once())->method('addReadStream'); | |
742 | $loop->expects($this->once())->method('removeReadStream'); | |
743 | ||
744 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
745 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); | |
746 | $loop->expects($this->once())->method('cancelTimer')->with($timer); | |
747 | ||
748 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
749 | $address = stream_socket_get_name($server, false); | |
750 | $executor = new TcpTransportExecutor($address, $loop); | |
751 | ||
752 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
753 | ||
754 | $promise = $executor->query($query); | |
755 | ||
756 | // use outgoing buffer as response message | |
757 | $ref = new \ReflectionProperty($executor, 'writeBuffer'); | |
758 | $ref->setAccessible(true); | |
759 | $data = $ref->getValue($executor); | |
760 | ||
761 | $client = stream_socket_accept($server); | |
762 | fwrite($client, $data); | |
763 | ||
764 | $executor->handleWritable(); | |
765 | $executor->handleRead(); | |
766 | ||
767 | $promise->then($this->expectCallableOnce()); | |
768 | ||
769 | // trigger connection close condition | |
770 | fclose($client); | |
771 | $executor->handleRead(); | |
772 | } | |
773 | ||
774 | public function testQueryAgainAfterPreviousQueryResolvedWillReuseSocketAndCancelIdleTimer() | |
775 | { | |
776 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
777 | $loop->expects($this->exactly(2))->method('addWriteStream'); | |
778 | $loop->expects($this->once())->method('removeWriteStream'); | |
779 | $loop->expects($this->once())->method('addReadStream'); | |
780 | $loop->expects($this->never())->method('removeReadStream'); | |
781 | ||
782 | $timer = $this->getMockBuilder('React\EventLoop\TimerInterface')->getMock(); | |
783 | $loop->expects($this->once())->method('addTimer')->with(0.001, $this->anything())->willReturn($timer); | |
784 | $loop->expects($this->once())->method('cancelTimer')->with($timer); | |
785 | ||
786 | $server = stream_socket_server('tcp://127.0.0.1:0'); | |
787 | $address = stream_socket_get_name($server, false); | |
788 | $executor = new TcpTransportExecutor($address, $loop); | |
789 | ||
790 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
791 | ||
792 | $promise = $executor->query($query); | |
793 | ||
794 | // use outgoing buffer as response message | |
795 | $ref = new \ReflectionProperty($executor, 'writeBuffer'); | |
796 | $ref->setAccessible(true); | |
797 | $data = $ref->getValue($executor); | |
798 | ||
799 | $client = stream_socket_accept($server); | |
800 | fwrite($client, $data); | |
801 | ||
802 | $executor->handleWritable(); | |
803 | $executor->handleRead(); | |
804 | ||
805 | $promise->then($this->expectCallableOnce()); | |
806 | ||
807 | // trigger second query | |
808 | $executor->query($query); | |
809 | } | |
810 | } |
28 | 28 | $this->wrapped |
29 | 29 | ->expects($this->once()) |
30 | 30 | ->method('query') |
31 | ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { | |
31 | ->will($this->returnCallback(function ($query) use (&$cancelled) { | |
32 | 32 | $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { |
33 | 33 | ++$cancelled; |
34 | 34 | $reject(new CancellationException('Cancelled')); |
37 | 37 | return $deferred->promise(); |
38 | 38 | })); |
39 | 39 | |
40 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
41 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
40 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
41 | $promise = $this->executor->query($query); | |
42 | 42 | |
43 | 43 | $this->assertEquals(0, $cancelled); |
44 | 44 | $promise->cancel(); |
54 | 54 | ->method('query') |
55 | 55 | ->willReturn(Promise\resolve('0.0.0.0')); |
56 | 56 | |
57 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
58 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
57 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
58 | $promise = $this->executor->query($query); | |
59 | 59 | |
60 | 60 | $promise->then($this->expectCallableOnce(), $this->expectCallableNever()); |
61 | 61 | } |
67 | 67 | ->method('query') |
68 | 68 | ->willReturn(Promise\reject(new \RuntimeException())); |
69 | 69 | |
70 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
71 | $promise = $this->executor->query('8.8.8.8:53', $query); | |
70 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
71 | $promise = $this->executor->query($query); | |
72 | 72 | |
73 | 73 | $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith(new \RuntimeException())); |
74 | 74 | } |
82 | 82 | $this->wrapped |
83 | 83 | ->expects($this->once()) |
84 | 84 | ->method('query') |
85 | ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) { | |
85 | ->will($this->returnCallback(function ($query) use (&$cancelled) { | |
86 | 86 | $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { |
87 | 87 | ++$cancelled; |
88 | 88 | $reject(new CancellationException('Cancelled')); |
102 | 102 | $this->attribute($this->equalTo('DNS query for igor.io timed out'), 'message') |
103 | 103 | )); |
104 | 104 | |
105 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
106 | $this->executor->query('8.8.8.8:53', $query)->then($callback, $errorback); | |
105 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN); | |
106 | $this->executor->query($query)->then($callback, $errorback); | |
107 | 107 | |
108 | 108 | $this->assertEquals(0, $cancelled); |
109 | 109 |
11 | 11 | |
12 | 12 | class UdpTransportExecutorTest extends TestCase |
13 | 13 | { |
14 | /** | |
15 | * @dataProvider provideDefaultPortProvider | |
16 | * @param string $input | |
17 | * @param string $expected | |
18 | */ | |
19 | public function testCtorShouldAcceptNameserverAddresses($input, $expected) | |
20 | { | |
21 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
22 | ||
23 | $executor = new UdpTransportExecutor($input, $loop); | |
24 | ||
25 | $ref = new \ReflectionProperty($executor, 'nameserver'); | |
26 | $ref->setAccessible(true); | |
27 | $value = $ref->getValue($executor); | |
28 | ||
29 | $this->assertEquals($expected, $value); | |
30 | } | |
31 | ||
32 | public static function provideDefaultPortProvider() | |
33 | { | |
34 | return array( | |
35 | array( | |
36 | '8.8.8.8', | |
37 | 'udp://8.8.8.8:53' | |
38 | ), | |
39 | array( | |
40 | '1.2.3.4:5', | |
41 | 'udp://1.2.3.4:5' | |
42 | ), | |
43 | array( | |
44 | 'udp://1.2.3.4', | |
45 | 'udp://1.2.3.4:53' | |
46 | ), | |
47 | array( | |
48 | 'udp://1.2.3.4:53', | |
49 | 'udp://1.2.3.4:53' | |
50 | ), | |
51 | array( | |
52 | '::1', | |
53 | 'udp://[::1]:53' | |
54 | ), | |
55 | array( | |
56 | '[::1]:53', | |
57 | 'udp://[::1]:53' | |
58 | ) | |
59 | ); | |
60 | } | |
61 | ||
62 | /** | |
63 | * @expectedException InvalidArgumentException | |
64 | */ | |
65 | public function testCtorShouldThrowWhenNameserverAddressIsInvalid() | |
66 | { | |
67 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
68 | ||
69 | new UdpTransportExecutor('///', $loop); | |
70 | } | |
71 | ||
72 | /** | |
73 | * @expectedException InvalidArgumentException | |
74 | */ | |
75 | public function testCtorShouldThrowWhenNameserverAddressContainsHostname() | |
76 | { | |
77 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
78 | ||
79 | new UdpTransportExecutor('localhost', $loop); | |
80 | } | |
81 | ||
82 | /** | |
83 | * @expectedException InvalidArgumentException | |
84 | */ | |
85 | public function testCtorShouldThrowWhenNameserverSchemeIsInvalid() | |
86 | { | |
87 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
88 | ||
89 | new UdpTransportExecutor('tcp://1.2.3.4', $loop); | |
90 | } | |
91 | ||
14 | 92 | public function testQueryRejectsIfMessageExceedsUdpSize() |
15 | 93 | { |
16 | 94 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); |
17 | 95 | $loop->expects($this->never())->method('addReadStream'); |
18 | 96 | |
19 | $dumper = $this->getMockBuilder('React\Dns\Protocol\BinaryDumper')->getMock(); | |
20 | $dumper->expects($this->once())->method('toBinary')->willReturn(str_repeat('.', 513)); | |
21 | ||
22 | $executor = new UdpTransportExecutor($loop, null, $dumper); | |
23 | ||
24 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
25 | $promise = $executor->query('8.8.8.8:53', $query); | |
97 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
98 | ||
99 | $query = new Query('google.' . str_repeat('.com', 200), Message::TYPE_A, Message::CLASS_IN); | |
100 | $promise = $executor->query($query); | |
101 | ||
102 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
103 | ||
104 | $exception = null; | |
105 | $promise->then(null, function ($reason) use (&$exception) { | |
106 | $exception = $reason; | |
107 | }); | |
108 | ||
109 | $this->setExpectedException('RuntimeException', '', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); | |
110 | throw $exception; | |
111 | } | |
112 | ||
113 | public function testQueryRejectsIfServerConnectionFails() | |
114 | { | |
115 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
116 | $loop->expects($this->never())->method('addReadStream'); | |
117 | ||
118 | $executor = new UdpTransportExecutor('::1', $loop); | |
119 | ||
120 | $ref = new \ReflectionProperty($executor, 'nameserver'); | |
121 | $ref->setAccessible(true); | |
122 | $ref->setValue($executor, '///'); | |
123 | ||
124 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
125 | $promise = $executor->query($query); | |
26 | 126 | |
27 | 127 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); |
28 | 128 | $promise->then(null, $this->expectCallableOnce()); |
29 | 129 | } |
30 | 130 | |
31 | public function testQueryRejectsIfServerConnectionFails() | |
32 | { | |
33 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
34 | $loop->expects($this->never())->method('addReadStream'); | |
35 | ||
36 | $executor = new UdpTransportExecutor($loop); | |
37 | ||
38 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
39 | $promise = $executor->query('///', $query); | |
131 | /** | |
132 | * @group internet | |
133 | */ | |
134 | public function testQueryRejectsOnCancellation() | |
135 | { | |
136 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
137 | $loop->expects($this->once())->method('addReadStream'); | |
138 | $loop->expects($this->once())->method('removeReadStream'); | |
139 | ||
140 | $executor = new UdpTransportExecutor('8.8.8.8:53', $loop); | |
141 | ||
142 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
143 | $promise = $executor->query($query); | |
144 | $promise->cancel(); | |
40 | 145 | |
41 | 146 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); |
42 | 147 | $promise->then(null, $this->expectCallableOnce()); |
43 | 148 | } |
44 | 149 | |
45 | /** | |
46 | * @group internet | |
47 | */ | |
48 | public function testQueryRejectsOnCancellation() | |
49 | { | |
50 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
51 | $loop->expects($this->once())->method('addReadStream'); | |
52 | $loop->expects($this->once())->method('removeReadStream'); | |
53 | ||
54 | $executor = new UdpTransportExecutor($loop); | |
55 | ||
56 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
57 | $promise = $executor->query('8.8.8.8:53', $query); | |
58 | $promise->cancel(); | |
59 | ||
60 | $this->assertInstanceOf('React\Promise\PromiseInterface', $promise); | |
61 | $promise->then(null, $this->expectCallableOnce()); | |
62 | } | |
63 | ||
64 | 150 | public function testQueryKeepsPendingIfServerRejectsNetworkPacket() |
65 | 151 | { |
66 | 152 | $loop = Factory::create(); |
67 | 153 | |
68 | $executor = new UdpTransportExecutor($loop); | |
154 | $executor = new UdpTransportExecutor('127.0.0.1:1', $loop); | |
69 | 155 | |
70 | 156 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); |
71 | 157 | |
72 | 158 | $wait = true; |
73 | $promise = $executor->query('127.0.0.1:1', $query)->then( | |
159 | $promise = $executor->query($query)->then( | |
74 | 160 | null, |
75 | 161 | function ($e) use (&$wait) { |
76 | 162 | $wait = false; |
82 | 168 | $this->assertTrue($wait); |
83 | 169 | } |
84 | 170 | |
85 | public function testQueryKeepsPendingIfServerSendInvalidMessage() | |
171 | public function testQueryKeepsPendingIfServerSendsInvalidMessage() | |
86 | 172 | { |
87 | 173 | $loop = Factory::create(); |
88 | 174 | |
93 | 179 | }); |
94 | 180 | |
95 | 181 | $address = stream_socket_get_name($server, false); |
96 | $executor = new UdpTransportExecutor($loop); | |
182 | $executor = new UdpTransportExecutor($address, $loop); | |
97 | 183 | |
98 | 184 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); |
99 | 185 | |
100 | 186 | $wait = true; |
101 | $promise = $executor->query($address, $query)->then( | |
187 | $promise = $executor->query($query)->then( | |
102 | 188 | null, |
103 | 189 | function ($e) use (&$wait) { |
104 | 190 | $wait = false; |
110 | 196 | $this->assertTrue($wait); |
111 | 197 | } |
112 | 198 | |
113 | public function testQueryKeepsPendingIfServerSendInvalidId() | |
199 | public function testQueryKeepsPendingIfServerSendsInvalidId() | |
114 | 200 | { |
115 | 201 | $parser = new Parser(); |
116 | 202 | $dumper = new BinaryDumper(); |
122 | 208 | $data = stream_socket_recvfrom($server, 512, 0, $peer); |
123 | 209 | |
124 | 210 | $message = $parser->parseMessage($data); |
125 | $message->header->set('id', 0); | |
211 | $message->id = 0; | |
126 | 212 | |
127 | 213 | stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); |
128 | 214 | }); |
129 | 215 | |
130 | 216 | $address = stream_socket_get_name($server, false); |
131 | $executor = new UdpTransportExecutor($loop, $parser, $dumper); | |
217 | $executor = new UdpTransportExecutor($address, $loop); | |
132 | 218 | |
133 | 219 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); |
134 | 220 | |
135 | 221 | $wait = true; |
136 | $promise = $executor->query($address, $query)->then( | |
222 | $promise = $executor->query($query)->then( | |
137 | 223 | null, |
138 | 224 | function ($e) use (&$wait) { |
139 | 225 | $wait = false; |
157 | 243 | $data = stream_socket_recvfrom($server, 512, 0, $peer); |
158 | 244 | |
159 | 245 | $message = $parser->parseMessage($data); |
160 | $message->header->set('tc', 1); | |
246 | $message->tc = true; | |
161 | 247 | |
162 | 248 | stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer); |
163 | 249 | }); |
164 | 250 | |
165 | 251 | $address = stream_socket_get_name($server, false); |
166 | $executor = new UdpTransportExecutor($loop, $parser, $dumper); | |
167 | ||
168 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
169 | ||
170 | $wait = true; | |
171 | $promise = $executor->query($address, $query)->then( | |
172 | null, | |
173 | function ($e) use (&$wait) { | |
174 | $wait = false; | |
175 | throw $e; | |
176 | } | |
177 | ); | |
178 | ||
179 | // run loop for short period to ensure we detect connection ICMP rejection error | |
180 | \Clue\React\Block\sleep(0.01, $loop); | |
181 | if ($wait) { | |
182 | \Clue\React\Block\sleep(0.2, $loop); | |
183 | } | |
184 | ||
185 | $this->assertFalse($wait); | |
252 | $executor = new UdpTransportExecutor($address, $loop); | |
253 | ||
254 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
255 | ||
256 | $promise = $executor->query($query); | |
257 | ||
258 | $this->setExpectedException('RuntimeException', '', defined('SOCKET_EMSGSIZE') ? SOCKET_EMSGSIZE : 90); | |
259 | \Clue\React\Block\await($promise, $loop, 0.1); | |
186 | 260 | } |
187 | 261 | |
188 | 262 | public function testQueryResolvesIfServerSendsValidResponse() |
202 | 276 | }); |
203 | 277 | |
204 | 278 | $address = stream_socket_get_name($server, false); |
205 | $executor = new UdpTransportExecutor($loop, $parser, $dumper); | |
206 | ||
207 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
208 | ||
209 | $promise = $executor->query($address, $query); | |
279 | $executor = new UdpTransportExecutor($address, $loop); | |
280 | ||
281 | $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN); | |
282 | ||
283 | $promise = $executor->query($query); | |
210 | 284 | $response = \Clue\React\Block\await($promise, $loop, 0.2); |
211 | 285 | |
212 | 286 | $this->assertInstanceOf('React\Dns\Model\Message', $response); |
18 | 18 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); |
19 | 19 | } |
20 | 20 | |
21 | /** @test */ | |
22 | public function createWithoutPortShouldCreateResolverWithDefaultPort() | |
23 | { | |
24 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
25 | ||
26 | $factory = new Factory(); | |
27 | $resolver = $factory->create('8.8.8.8', $loop); | |
28 | ||
29 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
30 | $this->assertSame('8.8.8.8:53', $this->getResolverPrivateMemberValue($resolver, 'nameserver')); | |
31 | } | |
32 | ||
33 | /** @test */ | |
34 | public function createCachedShouldCreateResolverWithCachedExecutor() | |
35 | { | |
36 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
37 | ||
38 | $factory = new Factory(); | |
39 | $resolver = $factory->createCached('8.8.8.8:53', $loop); | |
40 | ||
41 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
42 | $executor = $this->getResolverPrivateExecutor($resolver); | |
43 | $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); | |
44 | $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); | |
45 | $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); | |
46 | $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); | |
47 | $this->assertInstanceOf('React\Cache\ArrayCache', $recordCacheCache); | |
48 | } | |
49 | ||
50 | /** @test */ | |
51 | public function createCachedShouldCreateResolverWithCachedExecutorWithCustomCache() | |
52 | { | |
53 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
54 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
55 | ||
56 | $factory = new Factory(); | |
57 | $resolver = $factory->createCached('8.8.8.8:53', $loop, $cache); | |
58 | ||
59 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
60 | $executor = $this->getResolverPrivateExecutor($resolver); | |
61 | $this->assertInstanceOf('React\Dns\Query\CachedExecutor', $executor); | |
62 | $recordCache = $this->getCachedExecutorPrivateMemberValue($executor, 'cache'); | |
63 | $recordCacheCache = $this->getRecordCachePrivateMemberValue($recordCache, 'cache'); | |
64 | $this->assertInstanceOf('React\Cache\CacheInterface', $recordCacheCache); | |
65 | $this->assertSame($cache, $recordCacheCache); | |
21 | ||
22 | /** @test */ | |
23 | public function createWithoutSchemeShouldCreateResolverWithSelectiveUdpAndTcpExecutorStack() | |
24 | { | |
25 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
26 | ||
27 | $factory = new Factory(); | |
28 | $resolver = $factory->create('8.8.8.8:53', $loop); | |
29 | ||
30 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
31 | ||
32 | $coopExecutor = $this->getResolverPrivateExecutor($resolver); | |
33 | ||
34 | $this->assertInstanceOf('React\Dns\Query\CoopExecutor', $coopExecutor); | |
35 | ||
36 | $ref = new \ReflectionProperty($coopExecutor, 'executor'); | |
37 | $ref->setAccessible(true); | |
38 | $selectiveExecutor = $ref->getValue($coopExecutor); | |
39 | ||
40 | $this->assertInstanceOf('React\Dns\Query\SelectiveTransportExecutor', $selectiveExecutor); | |
41 | ||
42 | // udp below: | |
43 | ||
44 | $ref = new \ReflectionProperty($selectiveExecutor, 'datagramExecutor'); | |
45 | $ref->setAccessible(true); | |
46 | $retryExecutor = $ref->getValue($selectiveExecutor); | |
47 | ||
48 | $this->assertInstanceOf('React\Dns\Query\RetryExecutor', $retryExecutor); | |
49 | ||
50 | $ref = new \ReflectionProperty($retryExecutor, 'executor'); | |
51 | $ref->setAccessible(true); | |
52 | $timeoutExecutor = $ref->getValue($retryExecutor); | |
53 | ||
54 | $this->assertInstanceOf('React\Dns\Query\TimeoutExecutor', $timeoutExecutor); | |
55 | ||
56 | $ref = new \ReflectionProperty($timeoutExecutor, 'executor'); | |
57 | $ref->setAccessible(true); | |
58 | $udpExecutor = $ref->getValue($timeoutExecutor); | |
59 | ||
60 | $this->assertInstanceOf('React\Dns\Query\UdpTransportExecutor', $udpExecutor); | |
61 | ||
62 | // tcp below: | |
63 | ||
64 | $ref = new \ReflectionProperty($selectiveExecutor, 'streamExecutor'); | |
65 | $ref->setAccessible(true); | |
66 | $timeoutExecutor = $ref->getValue($selectiveExecutor); | |
67 | ||
68 | $this->assertInstanceOf('React\Dns\Query\TimeoutExecutor', $timeoutExecutor); | |
69 | ||
70 | $ref = new \ReflectionProperty($timeoutExecutor, 'executor'); | |
71 | $ref->setAccessible(true); | |
72 | $tcpExecutor = $ref->getValue($timeoutExecutor); | |
73 | ||
74 | $this->assertInstanceOf('React\Dns\Query\TcpTransportExecutor', $tcpExecutor); | |
75 | } | |
76 | ||
77 | /** @test */ | |
78 | public function createWithUdpSchemeShouldCreateResolverWithUdpExecutorStack() | |
79 | { | |
80 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
81 | ||
82 | $factory = new Factory(); | |
83 | $resolver = $factory->create('udp://8.8.8.8:53', $loop); | |
84 | ||
85 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
86 | ||
87 | $coopExecutor = $this->getResolverPrivateExecutor($resolver); | |
88 | ||
89 | $this->assertInstanceOf('React\Dns\Query\CoopExecutor', $coopExecutor); | |
90 | ||
91 | $ref = new \ReflectionProperty($coopExecutor, 'executor'); | |
92 | $ref->setAccessible(true); | |
93 | $retryExecutor = $ref->getValue($coopExecutor); | |
94 | ||
95 | $this->assertInstanceOf('React\Dns\Query\RetryExecutor', $retryExecutor); | |
96 | ||
97 | $ref = new \ReflectionProperty($retryExecutor, 'executor'); | |
98 | $ref->setAccessible(true); | |
99 | $timeoutExecutor = $ref->getValue($retryExecutor); | |
100 | ||
101 | $this->assertInstanceOf('React\Dns\Query\TimeoutExecutor', $timeoutExecutor); | |
102 | ||
103 | $ref = new \ReflectionProperty($timeoutExecutor, 'executor'); | |
104 | $ref->setAccessible(true); | |
105 | $udpExecutor = $ref->getValue($timeoutExecutor); | |
106 | ||
107 | $this->assertInstanceOf('React\Dns\Query\UdpTransportExecutor', $udpExecutor); | |
108 | } | |
109 | ||
110 | /** @test */ | |
111 | public function createWithTcpSchemeShouldCreateResolverWithTcpExecutorStack() | |
112 | { | |
113 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
114 | ||
115 | $factory = new Factory(); | |
116 | $resolver = $factory->create('tcp://8.8.8.8:53', $loop); | |
117 | ||
118 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
119 | ||
120 | $coopExecutor = $this->getResolverPrivateExecutor($resolver); | |
121 | ||
122 | $this->assertInstanceOf('React\Dns\Query\CoopExecutor', $coopExecutor); | |
123 | ||
124 | $ref = new \ReflectionProperty($coopExecutor, 'executor'); | |
125 | $ref->setAccessible(true); | |
126 | $timeoutExecutor = $ref->getValue($coopExecutor); | |
127 | ||
128 | $this->assertInstanceOf('React\Dns\Query\TimeoutExecutor', $timeoutExecutor); | |
129 | ||
130 | $ref = new \ReflectionProperty($timeoutExecutor, 'executor'); | |
131 | $ref->setAccessible(true); | |
132 | $tcpExecutor = $ref->getValue($timeoutExecutor); | |
133 | ||
134 | $this->assertInstanceOf('React\Dns\Query\TcpTransportExecutor', $tcpExecutor); | |
66 | 135 | } |
67 | 136 | |
68 | 137 | /** |
69 | 138 | * @test |
70 | * @dataProvider factoryShouldAddDefaultPortProvider | |
139 | * @expectedException InvalidArgumentException | |
71 | 140 | */ |
72 | public function factoryShouldAddDefaultPort($input, $expected) | |
73 | { | |
74 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
75 | ||
76 | $factory = new Factory(); | |
77 | $resolver = $factory->create($input, $loop); | |
78 | ||
79 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
80 | $this->assertSame($expected, $this->getResolverPrivateMemberValue($resolver, 'nameserver')); | |
81 | } | |
82 | ||
83 | public static function factoryShouldAddDefaultPortProvider() | |
84 | { | |
85 | return array( | |
86 | array('8.8.8.8', '8.8.8.8:53'), | |
87 | array('1.2.3.4:5', '1.2.3.4:5'), | |
88 | array('localhost', 'localhost:53'), | |
89 | array('localhost:1234', 'localhost:1234'), | |
90 | array('::1', '[::1]:53'), | |
91 | array('[::1]:53', '[::1]:53') | |
92 | ); | |
141 | public function createShouldThrowWhenNameserverIsInvalid() | |
142 | { | |
143 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
144 | ||
145 | $factory = new Factory(); | |
146 | $factory->create('///', $loop); | |
147 | } | |
148 | ||
149 | /** @test */ | |
150 | public function createCachedShouldCreateResolverWithCachingExecutor() | |
151 | { | |
152 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
153 | ||
154 | $factory = new Factory(); | |
155 | $resolver = $factory->createCached('8.8.8.8:53', $loop); | |
156 | ||
157 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
158 | $executor = $this->getResolverPrivateExecutor($resolver); | |
159 | $this->assertInstanceOf('React\Dns\Query\CachingExecutor', $executor); | |
160 | $cache = $this->getCachingExecutorPrivateMemberValue($executor, 'cache'); | |
161 | $this->assertInstanceOf('React\Cache\ArrayCache', $cache); | |
162 | } | |
163 | ||
164 | /** @test */ | |
165 | public function createCachedShouldCreateResolverWithCachingExecutorWithCustomCache() | |
166 | { | |
167 | $cache = $this->getMockBuilder('React\Cache\CacheInterface')->getMock(); | |
168 | $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock(); | |
169 | ||
170 | $factory = new Factory(); | |
171 | $resolver = $factory->createCached('8.8.8.8:53', $loop, $cache); | |
172 | ||
173 | $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver); | |
174 | $executor = $this->getResolverPrivateExecutor($resolver); | |
175 | $this->assertInstanceOf('React\Dns\Query\CachingExecutor', $executor); | |
176 | $cacheProperty = $this->getCachingExecutorPrivateMemberValue($executor, 'cache'); | |
177 | $this->assertSame($cache, $cacheProperty); | |
93 | 178 | } |
94 | 179 | |
95 | 180 | private function getResolverPrivateExecutor($resolver) |
114 | 199 | return $reflector->getValue($resolver); |
115 | 200 | } |
116 | 201 | |
117 | private function getCachedExecutorPrivateMemberValue($resolver, $field) | |
118 | { | |
119 | $reflector = new \ReflectionProperty('React\Dns\Query\CachedExecutor', $field); | |
202 | private function getCachingExecutorPrivateMemberValue($resolver, $field) | |
203 | { | |
204 | $reflector = new \ReflectionProperty('React\Dns\Query\CachingExecutor', $field); | |
120 | 205 | $reflector->setAccessible(true); |
121 | 206 | return $reflector->getValue($resolver); |
122 | 207 | } |
123 | ||
124 | private function getRecordCachePrivateMemberValue($resolver, $field) | |
125 | { | |
126 | $reflector = new \ReflectionProperty('React\Dns\Query\RecordCache', $field); | |
127 | $reflector->setAccessible(true); | |
128 | return $reflector->getValue($resolver); | |
129 | } | |
130 | 208 | } |
1 | 1 | |
2 | 2 | namespace React\Tests\Dns\Resolver; |
3 | 3 | |
4 | use PHPUnit\Framework\TestCase; | |
4 | use React\Tests\Dns\TestCase; | |
5 | 5 | use React\Dns\Resolver\Resolver; |
6 | use React\Dns\Query\Query; | |
7 | 6 | use React\Dns\Model\Message; |
8 | 7 | use React\Dns\Model\Record; |
9 | 8 | |
10 | 9 | class ResolveAliasesTest extends TestCase |
11 | 10 | { |
12 | 11 | /** |
13 | * @covers React\Dns\Resolver\Resolver::resolveAliases | |
14 | * @covers React\Dns\Resolver\Resolver::valuesByNameAndType | |
15 | 12 | * @dataProvider provideAliasedAnswers |
16 | 13 | */ |
17 | 14 | public function testResolveAliases(array $expectedAnswers, array $answers, $name) |
18 | 15 | { |
16 | $message = new Message(); | |
17 | foreach ($answers as $answer) { | |
18 | $message->answers[] = $answer; | |
19 | } | |
20 | ||
19 | 21 | $executor = $this->createExecutorMock(); |
20 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
22 | $executor->expects($this->once())->method('query')->willReturn(\React\Promise\resolve($message)); | |
21 | 23 | |
22 | $answers = $resolver->resolveAliases($answers, $name); | |
24 | $resolver = new Resolver($executor); | |
23 | 25 | |
24 | $this->assertEquals($expectedAnswers, $answers); | |
26 | $answers = $resolver->resolveAll($name, Message::TYPE_A); | |
27 | ||
28 | $answers->then($this->expectCallableOnceWith($expectedAnswers), null); | |
25 | 29 | } |
26 | 30 | |
27 | 31 | public function provideAliasedAnswers() |
49 | 53 | new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), |
50 | 54 | new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), |
51 | 55 | new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'), |
52 | ), | |
53 | 'igor.io', | |
54 | ), | |
55 | array( | |
56 | array(), | |
57 | array( | |
58 | new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN), | |
59 | new Record('bar.igor.io', Message::TYPE_A, Message::CLASS_IN), | |
60 | 56 | ), |
61 | 57 | 'igor.io', |
62 | 58 | ), |
18 | 18 | $executor |
19 | 19 | ->expects($this->once()) |
20 | 20 | ->method('query') |
21 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
22 | ->will($this->returnCallback(function ($nameserver, $query) { | |
23 | $response = new Message(); | |
24 | $response->header->set('qr', 1); | |
25 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
21 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
22 | ->will($this->returnCallback(function ($query) { | |
23 | $response = new Message(); | |
24 | $response->qr = true; | |
25 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
26 | 26 | $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '178.79.169.131'); |
27 | 27 | |
28 | 28 | return Promise\resolve($response); |
29 | 29 | })); |
30 | 30 | |
31 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
31 | $resolver = new Resolver($executor); | |
32 | 32 | $resolver->resolve('igor.io')->then($this->expectCallableOnceWith('178.79.169.131')); |
33 | 33 | } |
34 | 34 | |
39 | 39 | $executor |
40 | 40 | ->expects($this->once()) |
41 | 41 | ->method('query') |
42 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
43 | ->will($this->returnCallback(function ($nameserver, $query) { | |
44 | $response = new Message(); | |
45 | $response->header->set('qr', 1); | |
46 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
42 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
43 | ->will($this->returnCallback(function ($query) { | |
44 | $response = new Message(); | |
45 | $response->qr = true; | |
46 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
47 | 47 | $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1'); |
48 | 48 | |
49 | 49 | return Promise\resolve($response); |
50 | 50 | })); |
51 | 51 | |
52 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
52 | $resolver = new Resolver($executor); | |
53 | 53 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1'))); |
54 | 54 | } |
55 | 55 | |
60 | 60 | $executor |
61 | 61 | ->expects($this->once()) |
62 | 62 | ->method('query') |
63 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
64 | ->will($this->returnCallback(function ($nameserver, $query) { | |
65 | $response = new Message(); | |
66 | $response->header->set('qr', 1); | |
67 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
63 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
64 | ->will($this->returnCallback(function ($query) { | |
65 | $response = new Message(); | |
66 | $response->qr = true; | |
67 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
68 | 68 | $response->answers[] = new Record($query->name, Message::TYPE_TXT, $query->class, 3600, array('ignored')); |
69 | 69 | $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1'); |
70 | 70 | |
71 | 71 | return Promise\resolve($response); |
72 | 72 | })); |
73 | 73 | |
74 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
74 | $resolver = new Resolver($executor); | |
75 | 75 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1'))); |
76 | 76 | } |
77 | 77 | |
82 | 82 | $executor |
83 | 83 | ->expects($this->once()) |
84 | 84 | ->method('query') |
85 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
86 | ->will($this->returnCallback(function ($nameserver, $query) { | |
87 | $response = new Message(); | |
88 | $response->header->set('qr', 1); | |
89 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
85 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
86 | ->will($this->returnCallback(function ($query) { | |
87 | $response = new Message(); | |
88 | $response->qr = true; | |
89 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
90 | 90 | $response->answers[] = new Record($query->name, Message::TYPE_CNAME, $query->class, 3600, 'example.com'); |
91 | 91 | $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::1'); |
92 | 92 | $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::2'); |
93 | $response->prepare(); | |
94 | ||
95 | return Promise\resolve($response); | |
96 | })); | |
97 | ||
98 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
93 | ||
94 | return Promise\resolve($response); | |
95 | })); | |
96 | ||
97 | $resolver = new Resolver($executor); | |
99 | 98 | $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then( |
100 | 99 | $this->expectCallableOnceWith($this->equalTo(array('::1', '::2'))) |
101 | 100 | ); |
108 | 107 | $executor |
109 | 108 | ->expects($this->once()) |
110 | 109 | ->method('query') |
111 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
112 | ->will($this->returnCallback(function ($nameserver, $query) { | |
113 | $response = new Message(); | |
114 | $response->header->set('qr', 1); | |
115 | $response->questions[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class); | |
110 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
111 | ->will($this->returnCallback(function ($query) { | |
112 | $response = new Message(); | |
113 | $response->qr = true; | |
114 | $response->questions[] = new Query('Blog.wyrihaximus.net', $query->type, $query->class); | |
116 | 115 | $response->answers[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class, 3600, '178.79.169.131'); |
117 | 116 | |
118 | 117 | return Promise\resolve($response); |
119 | 118 | })); |
120 | 119 | |
121 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
120 | $resolver = new Resolver($executor); | |
122 | 121 | $resolver->resolve('blog.wyrihaximus.net')->then($this->expectCallableOnceWith('178.79.169.131')); |
123 | 122 | } |
124 | 123 | |
129 | 128 | $executor |
130 | 129 | ->expects($this->once()) |
131 | 130 | ->method('query') |
132 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
133 | ->will($this->returnCallback(function ($nameserver, $query) { | |
134 | $response = new Message(); | |
135 | $response->header->set('qr', 1); | |
136 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
131 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
132 | ->will($this->returnCallback(function ($query) { | |
133 | $response = new Message(); | |
134 | $response->qr = true; | |
135 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
137 | 136 | $response->answers[] = new Record('foo.bar', $query->type, $query->class, 3600, '178.79.169.131'); |
138 | 137 | |
139 | 138 | return Promise\resolve($response); |
141 | 140 | |
142 | 141 | $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException')); |
143 | 142 | |
144 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
143 | $resolver = new Resolver($executor); | |
145 | 144 | $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); |
146 | 145 | } |
147 | 146 | |
154 | 153 | $executor |
155 | 154 | ->expects($this->once()) |
156 | 155 | ->method('query') |
157 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
158 | ->will($this->returnCallback(function ($nameserver, $query) { | |
159 | $response = new Message(); | |
160 | $response->header->set('qr', 1); | |
161 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
156 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
157 | ->will($this->returnCallback(function ($query) { | |
158 | $response = new Message(); | |
159 | $response->qr = true; | |
160 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
162 | 161 | |
163 | 162 | return Promise\resolve($response); |
164 | 163 | })); |
167 | 166 | return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io did not return a valid answer (NOERROR / NODATA)'); |
168 | 167 | })); |
169 | 168 | |
170 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
169 | $resolver = new Resolver($executor); | |
171 | 170 | $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback); |
172 | 171 | } |
173 | 172 | |
211 | 210 | $executor |
212 | 211 | ->expects($this->once()) |
213 | 212 | ->method('query') |
214 | ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query')) | |
215 | ->will($this->returnCallback(function ($nameserver, $query) use ($code) { | |
216 | $response = new Message(); | |
217 | $response->header->set('qr', 1); | |
218 | $response->header->set('rcode', $code); | |
219 | $response->questions[] = new Record($query->name, $query->type, $query->class); | |
213 | ->with($this->isInstanceOf('React\Dns\Query\Query')) | |
214 | ->will($this->returnCallback(function ($query) use ($code) { | |
215 | $response = new Message(); | |
216 | $response->qr = true; | |
217 | $response->rcode = $code; | |
218 | $response->questions[] = new Query($query->name, $query->type, $query->class); | |
220 | 219 | |
221 | 220 | return Promise\resolve($response); |
222 | 221 | })); |
225 | 224 | return ($param instanceof RecordNotFoundException && $param->getCode() === $code && $param->getMessage() === $expectedMessage); |
226 | 225 | })); |
227 | 226 | |
228 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
227 | $resolver = new Resolver($executor); | |
229 | 228 | $resolver->resolve('example.com')->then($this->expectCallableNever(), $errback); |
230 | 229 | } |
231 | 230 | |
232 | public function testLegacyExtractAddress() | |
233 | { | |
234 | $executor = $this->createExecutorMock(); | |
235 | $resolver = new Resolver('8.8.8.8:53', $executor); | |
236 | ||
237 | $query = new Query('reactphp.org', Message::TYPE_A, Message::CLASS_IN); | |
238 | $response = Message::createResponseWithAnswersForQuery($query, array( | |
239 | new Record('reactphp.org', Message::TYPE_A, Message::CLASS_IN, 3600, '1.2.3.4') | |
240 | )); | |
241 | ||
242 | $ret = $resolver->extractAddress($query, $response); | |
243 | $this->assertEquals('1.2.3.4', $ret); | |
244 | } | |
245 | ||
246 | 231 | private function createExecutorMock() |
247 | 232 | { |
248 | 233 | return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock(); |