Codebase list reactphp-dns / dcb38a9
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
58 changed file(s) with 4227 addition(s) and 2264 deletion(s). Raw diff Collapse all Expand all
77 - 7.0
88 - 7.1
99 - 7.2
10 - hhvm # ignore errors, see below
10 - 7.3
11 # - hhvm # requires legacy phpunit & ignore errors, see below
1112
1213 # lock distro so new future defaults will not break the build
1314 dist: trusty
1617 include:
1718 - php: 5.3
1819 dist: precise
20 - php: hhvm
21 install: composer require phpunit/phpunit:^5 --dev --no-interaction
1922 allow_failures:
2023 - php: hhvm
2124
00 # 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)
186
287 ## 0.4.16 (2018-11-11)
388
1212 * [Basic usage](#basic-usage)
1313 * [Caching](#caching)
1414 * [Custom cache adapter](#custom-cache-adapter)
15 * [Resolver](#resolver)
15 * [ResolverInterface](#resolverinterface)
1616 * [resolve()](#resolve)
1717 * [resolveAll()](#resolveall)
1818 * [Advanced usage](#advanced-usage)
1919 * [UdpTransportExecutor](#udptransportexecutor)
20 * [TcpTransportExecutor](#tcptransportexecutor)
21 * [SelectiveTransportExecutor](#selectivetransportexecutor)
2022 * [HostsFileExecutor](#hostsfileexecutor)
2123 * [Install](#install)
2224 * [Tests](#tests)
110112
111113 See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
112114
113 ## Resolver
115 ## ResolverInterface
116
117 <a id="resolver"><!-- legacy reference --></a>
114118
115119 ### resolve()
116120
207211
208212 ```php
209213 $loop = Factory::create();
210 $executor = new UdpTransportExecutor($loop);
214 $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
211215
212216 $executor->query(
213 '8.8.8.8:53',
214217 new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
215218 )->then(function (Message $message) {
216219 foreach ($message->answers as $answer) {
228231
229232 ```php
230233 $executor = new TimeoutExecutor(
231 new UdpTransportExecutor($loop),
234 new UdpTransportExecutor($nameserver, $loop),
232235 3.0,
233236 $loop
234237 );
241244 ```php
242245 $executor = new RetryExecutor(
243246 new TimeoutExecutor(
244 new UdpTransportExecutor($loop),
247 new UdpTransportExecutor($nameserver, $loop),
245248 3.0,
246249 $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 )
247270 )
248271 );
249272 ```
254277 packages. Higher-level components should take advantage of the Datagram
255278 component instead of reimplementing this socket logic from scratch.
256279
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
257401 ### HostsFileExecutor
258402
259403 Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
263407 ```php
264408 $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
265409
266 $executor = new UdpTransportExecutor($loop);
410 $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
267411 $executor = new HostsFileExecutor($hosts, $executor);
268412
269413 $executor->query(
270 '8.8.8.8:53',
271414 new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
272415 );
273416 ```
277420 The recommended way to install this library is [through Composer](https://getcomposer.org).
278421 [New to Composer?](https://getcomposer.org/doc/00-intro.md)
279422
423 This project follows [SemVer](https://semver.org/).
280424 This will install the latest supported version:
281425
282426 ```bash
283 $ composer require react/dns:^0.4.16
427 $ composer require react/dns:^1.2
284428 ```
285429
286430 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
44 "license": "MIT",
55 "require": {
66 "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"
1211 },
1312 "require-dev": {
1413 "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"
1615 },
1716 "autoload": {
1817 "psr-4": { "React\\Dns\\": "src" }
00 <?php
1
2 // $ php examples/12-all-types.php
3 // $ php examples/12-all-types.php myserverplace.de SSHFP
14
25 use React\Dns\Config\Config;
36 use React\Dns\Resolver\Factory;
77 require __DIR__ . '/../vendor/autoload.php';
88
99 $loop = Factory::create();
10 $executor = new UdpTransportExecutor($loop);
10 $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
1111
1212 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
1313
1414 $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN);
1515 $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN);
1616
17 $executor->query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) {
17 $executor->query($ipv4Query)->then(function (Message $message) {
1818 foreach ($message->answers as $answer) {
1919 echo 'IPv4: ' . $answer->data . PHP_EOL;
2020 }
2121 }, 'printf');
22 $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) {
22 $executor->query($ipv6Query)->then(function (Message $message) {
2323 foreach ($message->answers as $answer) {
2424 echo 'IPv6: ' . $answer->data . PHP_EOL;
2525 }
00 <?php
1
2 // $ php examples/92-query-any.php mailbox.org
3 // $ php examples/92-query-any.php _carddav._tcp.mailbox.org
14
25 use React\Dns\Model\Message;
36 use React\Dns\Model\Record;
47 use React\Dns\Query\Query;
5 use React\Dns\Query\UdpTransportExecutor;
8 use React\Dns\Query\TcpTransportExecutor;
69 use React\EventLoop\Factory;
710
811 require __DIR__ . '/../vendor/autoload.php';
912
1013 $loop = Factory::create();
11 $executor = new UdpTransportExecutor($loop);
14 $executor = new TcpTransportExecutor('8.8.8.8:53', $loop);
1215
1316 $name = isset($argv[1]) ? $argv[1] : 'google.com';
1417
1518 $any = new Query($name, Message::TYPE_ANY, Message::CLASS_IN);
1619
17 $executor->query('8.8.8.8:53', $any)->then(function (Message $message) {
20 $executor->query($any)->then(function (Message $message) {
1821 foreach ($message->answers as $answer) {
1922 /* @var $answer Record */
2023
4851 $data = implode(' ', $data);
4952 break;
5053 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
5255 $type = 'SRV';
5356 $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);
5462 break;
5563 case Message::TYPE_SOA:
5664 // SOA records contain structured data, dump structure here
5765 $type = 'SOA';
5866 $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'] . '"';
5972 break;
6073 default:
6174 // unknown type uses HEX format
77 convertWarningsToExceptions="true"
88 processIsolation="false"
99 stopOnFailure="false"
10 syntaxCheck="false"
1110 bootstrap="vendor/autoload.php"
1211 >
1312 <testsuites>
11
22 namespace React\Dns;
33
4 class BadServerException extends \Exception
4 final class BadServerException extends \Exception
55 {
66 }
33
44 use RuntimeException;
55
6 class Config
6 final class Config
77 {
88 /**
99 * Loads the system DNS configuration
+0
-73
src/Config/FilesystemFactory.php less more
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 }
7171
7272 return new self($contents);
7373 }
74
75 private $contents;
7476
7577 /**
7678 * Instantiate new hosts file with the given hosts file contents
+0
-59
src/Model/HeaderBag.php less more
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 }
33
44 use React\Dns\Query\Query;
55
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
712 {
813 const TYPE_A = 1;
914 const TYPE_NS = 2;
1419 const TYPE_TXT = 16;
1520 const TYPE_AAAA = 28;
1621 const TYPE_SRV = 33;
22 const TYPE_SSHFP = 44;
1723 const TYPE_ANY = 255;
24 const TYPE_CAA = 257;
1825
1926 const CLASS_IN = 1;
2027
3845 public static function createRequestForQuery(Query $query)
3946 {
4047 $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;
4551
4652 return $request;
4753 }
5662 public static function createResponseWithAnswersForQuery(Query $query, array $answers)
5763 {
5864 $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;
6468
65 $response->questions[] = (array) $query;
69 $response->questions[] = $query;
6670
6771 foreach ($answers as $record) {
6872 $response->answers[] = $record;
6973 }
70
71 $response->prepare();
7274
7375 return $response;
7476 }
99101 return mt_rand(0, 0xffff);
100102 }
101103
102 public $header;
103 public $questions = array();
104 public $answers = array();
105 public $authority = array();
106 public $additional = array();
107
108104 /**
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
125106 *
126107 * The response message ID has to match the request message ID. This allows
127108 * the receiver to verify this is the correct response message. An outside
128109 * attacker may try to inject fake responses by "guessing" the message ID,
129110 * so this should use a proper CSPRNG to avoid possible cache poisoning.
130111 *
131 * @return int
112 * @var int 16 bit message ID
132113 * @see self::generateId()
133114 */
134 public function getId()
135 {
136 return $this->header->get('id');
137 }
115 public $id = 0;
138116
139117 /**
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 /**
141129 *
142 * @return int see self::RCODE_* constants
130 * @var bool Authoritative Answer
143131 */
144 public function getResponseCode()
145 {
146 return $this->header->get('rcode');
147 }
132 public $aa = false;
148133
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();
153186 }
11
22 namespace React\Dns\Model;
33
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
514 {
615 /**
716 * @var string hostname without trailing dot, for example "reactphp.org"
3342 *
3443 * - A:
3544 * IPv4 address string, for example "192.168.1.1".
45 *
3646 * - AAAA:
3747 * IPv6 address string, for example "::1".
48 *
3849 * - CNAME / PTR / NS:
3950 * The hostname without trailing dot, for example "reactphp.org".
51 *
4052 * - TXT:
4153 * List of string values, for example `["v=spf1 include:example.com"]`.
4254 * This is commonly a list with only a single string value, but this
4860 * suggests using key-value pairs such as `["name=test","version=1"]`, but
4961 * interpretation of this is not enforced and left up to consumers of this
5062 * library (used for DNS-SD/Zeroconf and others).
63 *
5164 * - MX:
5265 * Mail server priority (UINT16) and target hostname without trailing dot,
5366 * for example `{"priority":10,"target":"mx.example.com"}`.
5669 * referred to as exchange). If a response message contains multiple
5770 * records of this type, targets should be sorted by priority (lowest
5871 * first) - this is left up to consumers of this library (used for SMTP).
72 *
5973 * - SRV:
6074 * Service priority (UINT16), service weight (UINT16), service port (UINT16)
6175 * and target hostname without trailing dot, for example
6882 * randomly according to their weight - this is left up to consumers of
6983 * this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782)
7084 * 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 *
7193 * - SOA:
7294 * Includes master hostname without trailing dot, responsible person email
7395 * as hostname without trailing dot and serial, refresh, retry, expire and
7496 * minimum times in seconds (UINT32 each), for example:
7597 * `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial":
7698 * 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 *
77104 * - Any other unknown type:
78105 * An opaque binary string containing the RDATA as transported in the DNS
79106 * record. For forwards compatibility, you should not rely on this format
86113 */
87114 public $data;
88115
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)
90124 {
91125 $this->name = $name;
92126 $this->type = $type;
22 namespace React\Dns\Protocol;
33
44 use React\Dns\Model\Message;
5 use React\Dns\Model\HeaderBag;
5 use React\Dns\Model\Record;
6 use React\Dns\Query\Query;
67
7 class BinaryDumper
8 final class BinaryDumper
89 {
10 /**
11 * @param Message $message
12 * @return string
13 */
914 public function toBinary(Message $message)
1015 {
1116 $data = '';
1217
13 $data .= $this->headerToBinary($message->header);
18 $data .= $this->headerToBinary($message);
1419 $data .= $this->questionToBinary($message->questions);
20 $data .= $this->recordsToBinary($message->answers);
21 $data .= $this->recordsToBinary($message->authority);
22 $data .= $this->recordsToBinary($message->additional);
1523
1624 return $data;
1725 }
1826
19 private function headerToBinary(HeaderBag $header)
27 /**
28 * @param Message $message
29 * @return string
30 */
31 private function headerToBinary(Message $message)
2032 {
2133 $data = '';
2234
23 $data .= pack('n', $header->get('id'));
35 $data .= pack('n', $message->id);
2436
2537 $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;
3446
3547 $data .= pack('n', $flags);
3648
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));
4153
4254 return $data;
4355 }
4456
57 /**
58 * @param Query[] $questions
59 * @return string
60 */
4561 private function questionToBinary(array $questions)
4662 {
4763 $data = '';
4864
4965 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);
5768 }
5869
5970 return $data;
6071 }
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 }
61188 }
33
44 use React\Dns\Model\Message;
55 use React\Dns\Model\Record;
6 use React\Dns\Query\Query;
67 use InvalidArgumentException;
78
89 /**
1011 *
1112 * Obsolete and uncommon types and classes are not implemented.
1213 */
13 class Parser
14 final class Parser
1415 {
1516 /**
1617 * Parses the given raw binary message into a Message object
2122 */
2223 public function parseMessage($data)
2324 {
25 // create empty message with two additional, temporary properties for parser
2426 $message = new Message();
27 $message->data = $data;
28 $message->consumed = null;
29
2530 if ($this->parse($data, $message) !== $message) {
2631 throw new InvalidArgumentException('Unable to parse binary message');
2732 }
2833
34 unset($message->data, $message->consumed);
35
2936 return $message;
3037 }
3138
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
4139 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)
6740 {
6841 if (!isset($message->data[12 - 1])) {
6942 return;
7043 }
7144
72 $header = substr($message->data, 0, 12);
45 list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($message->data, 0, 12)));
7346 $message->consumed += 12;
7447
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 }
9295 }
9396
9497 return $message;
9598 }
9699
97 public function parseQuestion(Message $message)
100 /**
101 * @param Message $message
102 * @return ?Query
103 */
104 private function parseQuestion(Message $message)
98105 {
99106 $consumed = $message->consumed;
100107
109116
110117 $message->consumed = $consumed;
111118
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
116123 );
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)
126131 {
127132 $consumed = $message->consumed;
128133
129134 list($name, $consumed) = $this->readDomain($message->data, $consumed);
130135
131136 if ($name === null || !isset($message->data[$consumed + 10 - 1])) {
132 return;
137 return null;
133138 }
134139
135140 list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
147152 $consumed += 2;
148153
149154 if (!isset($message->data[$consumed + $rdLength - 1])) {
150 return;
155 return null;
151156 }
152157
153158 $rdata = null;
192197 'weight' => $weight,
193198 'port' => $port,
194199 '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
195212 );
196213 }
197214 } elseif (Message::TYPE_SOA === $type) {
212229 'minimum' => $minimum
213230 );
214231 }
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 }
215248 } else {
216249 // unknown types simply parse rdata as an opaque binary string
217250 $rdata = substr($message->data, $consumed, $rdLength);
220253
221254 // ensure parsing record data consumes expact number of bytes indicated in record length
222255 if ($consumed !== $expected || $rdata === null) {
223 return;
256 return null;
224257 }
225258
226259 $message->consumed = $consumed;
227260
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);
237262 }
238263
239264 private function readDomain($data, $consumed)
244269 return array(null, null);
245270 }
246271
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 );
248285 }
249286
250287 private function readLabels($data, $consumed)
293330
294331 return array($labels, $consumed);
295332 }
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 }
351333 }
+0
-55
src/Query/CachedExecutor.php less more
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 }
11
22 namespace React\Dns\Query;
33
4 class CancellationException extends \RuntimeException
4 final class CancellationException extends \RuntimeException
55 {
66 }
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
-160
src/Query/Executor.php less more
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 }
33
44 interface ExecutorInterface
55 {
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);
742 }
77 use React\Promise;
88
99 /**
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
1111 *
1212 * If the host is found in the hosts file, it will not be passed to the actual
1313 * DNS executor. If the host is not found in the hosts file, it will be passed
1414 * to the DNS executor as a fallback.
1515 */
16 class HostsFileExecutor implements ExecutorInterface
16 final class HostsFileExecutor implements ExecutorInterface
1717 {
1818 private $hosts;
1919 private $fallback;
2424 $this->fallback = $fallback;
2525 }
2626
27 public function query($nameserver, Query $query)
27 public function query(Query $query)
2828 {
2929 if ($query->class === Message::CLASS_IN && ($query->type === Message::TYPE_A || $query->type === Message::TYPE_AAAA)) {
3030 // forward lookup for type A or AAAA
6060 }
6161 }
6262
63 return $this->fallback->query($nameserver, $query);
63 return $this->fallback->query($query);
6464 }
6565
6666 private function getIpFromHost($host)
11
22 namespace React\Dns\Query;
33
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
514 {
15 /**
16 * @var string query name, i.e. hostname to look up
17 */
618 public $name;
19
20 /**
21 * @var int query type (aka QTYPE), see Message::TYPE_* constants
22 */
723 public $type;
24
25 /**
26 * @var int query class (aka QCLASS), see Message::CLASS_IN constant
27 */
828 public $class;
929
1030 /**
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
1234 */
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)
2236 {
23 if($currentTime === null) {
24 $currentTime = time();
25 }
26
2737 $this->name = $name;
2838 $this->type = $type;
2939 $this->class = $class;
30 $this->currentTime = $currentTime;
3140 }
3241 }
+0
-26
src/Query/RecordBag.php less more
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
-123
src/Query/RecordCache.php less more
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 }
44 use React\Promise\CancellablePromiseInterface;
55 use React\Promise\Deferred;
66
7 class RetryExecutor implements ExecutorInterface
7 final class RetryExecutor implements ExecutorInterface
88 {
99 private $executor;
1010 private $retries;
1515 $this->retries = $retries;
1616 }
1717
18 public function query($nameserver, Query $query)
18 public function query(Query $query)
1919 {
20 return $this->tryQuery($nameserver, $query, $this->retries);
20 return $this->tryQuery($query, $this->retries);
2121 }
2222
23 public function tryQuery($nameserver, Query $query, $retries)
23 public function tryQuery(Query $query, $retries)
2424 {
2525 $deferred = new Deferred(function () use (&$promise) {
2626 if ($promise instanceof CancellablePromiseInterface) {
3434 };
3535
3636 $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) {
3838 if (!$e instanceof TimeoutException) {
3939 $errorback = null;
4040 $deferred->reject($e);
6161 $r->setValue($e, $trace);
6262 } else {
6363 --$retries;
64 $promise = $executor->query($nameserver, $query)->then(
64 $promise = $executor->query($query)->then(
6565 $success,
6666 $errorback
6767 );
6868 }
6969 };
7070
71 $promise = $this->executor->query($nameserver, $query)->then(
71 $promise = $this->executor->query($query)->then(
7272 $success,
7373 $errorback
7474 );
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 }
11
22 namespace React\Dns\Query;
33
4 class TimeoutException extends \Exception
4 final class TimeoutException extends \Exception
55 {
66 }
66 use React\Promise\CancellablePromiseInterface;
77 use React\Promise\Timer;
88
9 class TimeoutExecutor implements ExecutorInterface
9 final class TimeoutExecutor implements ExecutorInterface
1010 {
1111 private $executor;
1212 private $loop;
1919 $this->timeout = $timeout;
2020 }
2121
22 public function query($nameserver, Query $query)
22 public function query(Query $query)
2323 {
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) {
2525 if ($e instanceof Timer\TimeoutException) {
2626 $e = new TimeoutException(sprintf("DNS query for %s timed out", $query->name), 0, $e);
2727 }
1818 *
1919 * ```php
2020 * $loop = Factory::create();
21 * $executor = new UdpTransportExecutor($loop);
21 * $executor = new UdpTransportExecutor('8.8.8.8:53', $loop);
2222 *
2323 * $executor->query(
24 * '8.8.8.8:53',
2524 * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
2625 * )->then(function (Message $message) {
2726 * foreach ($message->answers as $answer) {
3938 *
4039 * ```php
4140 * $executor = new TimeoutExecutor(
42 * new UdpTransportExecutor($loop),
41 * new UdpTransportExecutor($nameserver, $loop),
4342 * 3.0,
4443 * $loop
4544 * );
5251 * ```php
5352 * $executor = new RetryExecutor(
5453 * new TimeoutExecutor(
55 * new UdpTransportExecutor($loop),
54 * new UdpTransportExecutor($nameserver, $loop),
5655 * 3.0,
5756 * $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 * )
5877 * )
5978 * );
6079 * ```
6584 * packages. Higher-level components should take advantage of the Datagram
6685 * component instead of reimplementing this socket logic from scratch.
6786 */
68 class UdpTransportExecutor implements ExecutorInterface
87 final class UdpTransportExecutor implements ExecutorInterface
6988 {
89 private $nameserver;
7090 private $loop;
7191 private $parser;
7292 private $dumper;
7393
7494 /**
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
7897 */
79 public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null)
98 public function __construct($nameserver, LoopInterface $loop)
8099 {
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 . ']';
86103 }
87104
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);
88111 $this->loop = $loop;
89 $this->parser = $parser;
90 $this->dumper = $dumper;
112 $this->parser = new Parser();
113 $this->dumper = new BinaryDumper();
91114 }
92115
93 public function query($nameserver, Query $query)
116 public function query(Query $query)
94117 {
95118 $request = Message::createRequestForQuery($query);
96119
97120 $queryData = $this->dumper->toBinary($request);
98121 if (isset($queryData[512])) {
99122 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
101125 ));
102126 }
103127
104128 // 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);
106130 if ($socket === false) {
107131 return \React\Promise\reject(new \RuntimeException(
108132 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')',
139163
140164 // ignore and await next if we received an unexpected response ID
141165 // 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) {
143167 return;
144168 }
145169
147171 $loop->removeReadStream($socket);
148172 \fclose($socket);
149173
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 ));
152179 return;
153180 }
154181
11
22 namespace React\Dns;
33
4 class RecordNotFoundException extends \Exception
4 final class RecordNotFoundException extends \Exception
55 {
66 }
44 use React\Cache\ArrayCache;
55 use React\Cache\CacheInterface;
66 use React\Dns\Config\HostsFile;
7 use React\Dns\Query\CachedExecutor;
7 use React\Dns\Query\CachingExecutor;
8 use React\Dns\Query\CoopExecutor;
89 use React\Dns\Query\ExecutorInterface;
910 use React\Dns\Query\HostsFileExecutor;
10 use React\Dns\Query\RecordCache;
1111 use React\Dns\Query\RetryExecutor;
12 use React\Dns\Query\SelectiveTransportExecutor;
13 use React\Dns\Query\TcpTransportExecutor;
1214 use React\Dns\Query\TimeoutExecutor;
1315 use React\Dns\Query\UdpTransportExecutor;
1416 use React\EventLoop\LoopInterface;
1517
16 class Factory
18 final class Factory
1719 {
20 /**
21 * @param string $nameserver
22 * @param LoopInterface $loop
23 * @return \React\Dns\Resolver\ResolverInterface
24 */
1825 public function create($nameserver, LoopInterface $loop)
1926 {
20 $nameserver = $this->addPortToServerIfMissing($nameserver);
21 $executor = $this->decorateHostsFileExecutor($this->createRetryExecutor($loop));
27 $executor = $this->decorateHostsFileExecutor($this->createExecutor($nameserver, $loop));
2228
23 return new Resolver($nameserver, $executor);
29 return new Resolver($executor);
2430 }
2531
32 /**
33 * @param string $nameserver
34 * @param LoopInterface $loop
35 * @param ?CacheInterface $cache
36 * @return \React\Dns\Resolver\ResolverInterface
37 */
2638 public function createCached($nameserver, LoopInterface $loop, CacheInterface $cache = null)
2739 {
40 // default to keeping maximum of 256 responses in cache unless explicitly given
2841 if (!($cache instanceof CacheInterface)) {
29 $cache = new ArrayCache();
42 $cache = new ArrayCache(256);
3043 }
3144
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);
3448
35 return new Resolver($nameserver, $executor);
49 return new Resolver($executor);
3650 }
3751
3852 /**
6579 return $executor;
6680 }
6781
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)
69101 {
70102 return new TimeoutExecutor(
71 new UdpTransportExecutor($loop),
103 new TcpTransportExecutor($nameserver, $loop),
72104 5.0,
73105 $loop
74106 );
75107 }
76108
77 protected function createRetryExecutor(LoopInterface $loop)
109 private function createUdpExecutor($nameserver, LoopInterface $loop)
78110 {
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 );
99121 }
100122 }
55 use React\Dns\Query\ExecutorInterface;
66 use React\Dns\Query\Query;
77 use React\Dns\RecordNotFoundException;
8 use React\Promise\PromiseInterface;
98
10 class Resolver
9 /**
10 * @see ResolverInterface for the base interface
11 */
12 final class Resolver implements ResolverInterface
1113 {
12 private $nameserver;
1314 private $executor;
1415
15 public function __construct($nameserver, ExecutorInterface $executor)
16 public function __construct(ExecutorInterface $executor)
1617 {
17 $this->nameserver = $nameserver;
1818 $this->executor = $executor;
1919 }
2020
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 */
5921 public function resolve($domain)
6022 {
6123 return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
6325 });
6426 }
6527
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 */
11228 public function resolveAll($domain, $type)
11329 {
11430 $query = new Query($domain, $type, Message::CLASS_IN);
11531 $that = $this;
11632
11733 return $this->executor->query(
118 $this->nameserver,
11934 $query
12035 )->then(function (Message $response) use ($query, $that) {
12136 return $that->extractValues($query, $response);
12237 });
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)];
13338 }
13439
13540 /**
14449 public function extractValues(Query $query, Message $response)
14550 {
14651 // reject if response code indicates this is an error response message
147 $code = $response->getResponseCode();
52 $code = $response->rcode;
14853 if ($code !== Message::RCODE_OK) {
14954 switch ($code) {
15055 case Message::RCODE_FORMAT_ERROR:
18287 }
18388
18489 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);
19390 }
19491
19592 /**
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 }
9898 public function testLoadsFromWmicOnWindows()
9999 {
100100 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;
102106 }
103107
104108 $config = Config::loadWmicBlocking();
+0
-70
tests/Config/FilesystemFactoryTest.php less more
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 }
4646 /**
4747 * @group internet
4848 */
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 */
4977 public function testResolveAllGoogleMxResolvesWithCache()
5078 {
5179 $factory = new Factory();
5280 $this->resolver = $factory->createCached('8.8.8.8', $this->loop);
5381
5482 $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);
5596 $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever());
5697
5798 $this->loop->run();
97138 $promise = $this->resolver->resolve('google.com');
98139 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
99140 }
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 }
100211 }
99 {
1010 public function testCreateRequestDesiresRecusion()
1111 {
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);
1313 $request = Message::createRequestForQuery($query);
1414
15 $this->assertTrue($request->header->isQuery());
16 $this->assertSame(1, $request->header->get('rd'));
15 $this->assertFalse($request->qr);
16 $this->assertTrue($request->rd);
1717 }
1818
1919 public function testCreateResponseWithNoAnswers()
2020 {
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);
2222 $answers = array();
2323 $request = Message::createResponseWithAnswersForQuery($query, $answers);
2424
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);
2927 }
3028 }
22 namespace React\Tests\Dns\Protocol;
33
44 use PHPUnit\Framework\TestCase;
5 use React\Dns\Model\Message;
6 use React\Dns\Model\Record;
57 use React\Dns\Protocol\BinaryDumper;
6 use React\Dns\Model\Message;
8 use React\Dns\Query\Query;
79
810 class BinaryDumperTest extends TestCase
911 {
10 public function testRequestToBinary()
12 public function testToBinaryRequestMessage()
1113 {
1214 $data = "";
1315 $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header
1416 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
1517 $data .= "00 01 00 01"; // question: type A, class IN
1618
17 $expected = $this->formatHexDump(str_replace(' ', '', $data), 2);
19 $expected = $this->formatHexDump($data);
1820
1921 $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 );
3030
3131 $dumper = new BinaryDumper();
3232 $data = $dumper->toBinary($request);
3535 $this->assertSame($expected, $data);
3636 }
3737
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
38315 private function convertBinaryToHexDump($input)
39316 {
40317 return $this->formatHexDump(implode('', unpack('H*', $input)));
42319
43320 private function formatHexDump($input)
44321 {
45 return implode(' ', str_split($input, 2));
322 return implode(' ', str_split(str_replace(' ', '', $input), 2));
46323 }
47324 }
4141
4242 $request = $this->parser->parseMessage($data);
4343
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);
5855
5956 $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);
6360 }
6461
6562 public function testParseResponse()
7875
7976 $response = $this->parser->parseMessage($data);
8077
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);
9586
9687 $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);
10091
10192 $this->assertCount(1, $response->answers);
10293 $this->assertSame('igor.io', $response->answers[0]->name);
10697 $this->assertSame('178.79.169.131', $response->answers[0]->data);
10798 }
10899
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
112104 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
113105 $data .= "00 01 00 01"; // question: type A, class IN
114106 $data .= "03 77 77 77 04 69 67 6f 72 02 69 6f 00"; // question: www.igor.io
116108
117109 $data = $this->convertTcpDumpToBinary($data);
118110
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);
124112
125113 $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);
132120 }
133121
134122 public function testParseAnswerWithInlineData()
140128 $data .= "00 04"; // answer: rdlength 4
141129 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131
142130
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);
150132
151133 $this->assertCount(1, $response->answers);
152134 $this->assertSame('igor.io', $response->answers[0]->name);
165147 $data .= "00 04"; // answer: rdlength 4
166148 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131
167149
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);
175151
176152 $this->assertCount(1, $response->answers);
177153 $this->assertSame('igor.io', $response->answers[0]->name);
190166 $data .= "00 04"; // answer: rdlength 4
191167 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131
192168
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);
200170
201171 $this->assertCount(1, $response->answers);
202172 $this->assertSame('igor.io', $response->answers[0]->name);
215185 $data .= "00 04"; // answer: rdlength 4
216186 $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131
217187
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);
225189
226190 $this->assertCount(1, $response->answers);
227191 $this->assertSame('igor.io', $response->answers[0]->name);
240204 $data .= "00 05"; // answer: rdlength 5
241205 $data .= "68 65 6c 6c 6f"; // answer: rdata "hello"
242206
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);
250208
251209 $this->assertCount(1, $response->answers);
252210 $this->assertSame('igor.io', $response->answers[0]->name);
274232 $response = $this->parser->parseMessage($data);
275233
276234 $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);
280238
281239 $this->assertCount(1, $response->answers);
282240 $this->assertSame('mail.google.com', $response->answers[0]->name);
302260
303261 $response = $this->parser->parseMessage($data);
304262
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);
319271
320272 $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);
324276
325277 $this->assertCount(1, $response->answers);
326278 $this->assertSame('google.com', $response->answers[0]->name);
339291 $data .= "00 06"; // answer: rdlength 6
340292 $data .= "05 68 65 6c 6c 6f"; // answer: rdata length 5: hello
341293
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);
349295
350296 $this->assertCount(1, $response->answers);
351297 $this->assertSame('igor.io', $response->answers[0]->name);
364310 $data .= "00 0C"; // answer: rdlength 12
365311 $data .= "05 68 65 6c 6c 6f 05 77 6f 72 6c 64"; // answer: rdata length 5: hello, length 5: world
366312
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);
374314
375315 $this->assertCount(1, $response->answers);
376316 $this->assertSame('igor.io', $response->answers[0]->name);
389329 $data .= "00 09"; // answer: rdlength 9
390330 $data .= "00 0a 05 68 65 6c 6c 6f 00"; // answer: rdata priority 10: hello
391331
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);
399333
400334 $this->assertCount(1, $response->answers);
401335 $this->assertSame('igor.io', $response->answers[0]->name);
414348 $data .= "00 0C"; // answer: rdlength 12
415349 $data .= "00 0a 00 14 1F 90 04 74 65 73 74 00"; // answer: rdata priority 10, weight 20, port 8080 test
416350
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);
424352
425353 $this->assertCount(1, $response->answers);
426354 $this->assertSame('igor.io', $response->answers[0]->name);
438366 );
439367 }
440368
441 public function testParseResponseWithTwoAnswers()
369 public function testParseMessageResponseWithTwoAnswers()
442370 {
443371 $data = "";
444372 $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header
461389 $response = $this->parser->parseMessage($data);
462390
463391 $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);
467395
468396 $this->assertCount(2, $response->answers);
469397
480408 $this->assertSame('193.223.78.152', $response->answers[1]->data);
481409 }
482410
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
483500 public function testParseNSResponse()
484501 {
485502 $data = "";
489506 $data .= "00 07"; // answer: rdlength 7
490507 $data .= "05 68 65 6c 6c 6f 00"; // answer: rdata hello
491508
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);
499510
500511 $this->assertCount(1, $response->answers);
501512 $this->assertSame('igor.io', $response->answers[0]->name);
503514 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
504515 $this->assertSame(86400, $response->answers[0]->ttl);
505516 $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);
506536 }
507537
508538 public function testParseSOAResponse()
517547 $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600
518548 $data .= "00 09 3a 80 00 00 0e 10"; // answer: 605800, 3600
519549
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);
527551
528552 $this->assertCount(1, $response->answers);
529553 $this->assertSame('igor.io', $response->answers[0]->name);
544568 );
545569 }
546570
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
547591 public function testParsePTRResponse()
548592 {
549593 $data = "";
562606
563607 $response = $this->parser->parseMessage($data);
564608
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);
579617
580618 $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);
584622
585623 $this->assertCount(1, $response->answers);
586624 $this->assertSame('4.4.8.8.in-addr.arpa', $response->answers[0]->name);
588626 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
589627 $this->assertSame(86399, $response->answers[0]->ttl);
590628 $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);
591659 }
592660
593661 /**
699767 $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io
700768 $data .= "00 01 00 01"; // question: type A, class IN
701769 $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
702802
703803 $data = $this->convertTcpDumpToBinary($data);
704804
724824 $this->parser->parseMessage($data);
725825 }
726826
827 /**
828 * @expectedException InvalidArgumentException
829 */
727830 public function testParseInvalidNSResponseWhereDomainNameIsMissing()
728831 {
729832 $data = "";
732835 $data .= "00 01 51 80"; // answer: ttl 86400
733836 $data .= "00 00"; // answer: rdlength 0
734837
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 */
746844 public function testParseInvalidAResponseWhereIPIsMissing()
747845 {
748846 $data = "";
751849 $data .= "00 01 51 80"; // answer: ttl 86400
752850 $data .= "00 00"; // answer: rdlength 0
753851
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 */
765858 public function testParseInvalidAAAAResponseWhereIPIsMissing()
766859 {
767860 $data = "";
770863 $data .= "00 01 51 80"; // answer: ttl 86400
771864 $data .= "00 00"; // answer: rdlength 0
772865
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 */
784872 public function testParseInvalidTXTResponseWhereTxtChunkExceedsLimit()
785873 {
786874 $data = "";
790878 $data .= "00 06"; // answer: rdlength 6
791879 $data .= "06 68 65 6c 6c 6f 6f"; // answer: rdata length 6: helloo
792880
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 */
804887 public function testParseInvalidMXResponseWhereDomainNameIsIncomplete()
805888 {
806889 $data = "";
810893 $data .= "00 08"; // answer: rdlength 8
811894 $data .= "00 0a 05 68 65 6c 6c 6f"; // answer: rdata priority 10: hello (missing label end)
812895
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 */
824902 public function testParseInvalidMXResponseWhereDomainNameIsMissing()
825903 {
826904 $data = "";
830908 $data .= "00 02"; // answer: rdlength 2
831909 $data .= "00 0a"; // answer: rdata priority 10
832910
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 */
844917 public function testParseInvalidSRVResponseWhereDomainNameIsIncomplete()
845918 {
846919 $data = "";
850923 $data .= "00 0b"; // answer: rdlength 11
851924 $data .= "00 0a 00 14 1F 90 04 74 65 73 74"; // answer: rdata priority 10, weight 20, port 8080 test (missing label end)
852925
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 */
864932 public function testParseInvalidSRVResponseWhereDomainNameIsMissing()
865933 {
866934 $data = "";
870938 $data .= "00 06"; // answer: rdlength 6
871939 $data .= "00 0a 00 14 1F 90"; // answer: rdata priority 10, weight 20, port 8080
872940
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 */
884962 public function testParseInvalidSOAResponseWhereFlagsAreMissing()
885963 {
886964 $data = "";
891969 $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname)
892970 $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname)
893971
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);
9031018 }
9041019
9051020 private function convertTcpDumpToBinary($input)
9081023
9091024 return pack('H*', str_replace(' ', '', $input));
9101025 }
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 }
9111036 }
+0
-100
tests/Query/CachedExecutorTest.php less more
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
-308
tests/Query/ExecutorTest.php less more
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 }
2424 $this->hosts->expects($this->never())->method('getIpsForHost');
2525 $this->fallback->expects($this->once())->method('query');
2626
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));
2828 }
2929
3030 public function testFallsBackIfNoIpsWereFound()
3232 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array());
3333 $this->fallback->expects($this->once())->method('query');
3434
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));
3636 }
3737
3838 public function testReturnsResponseMessageIfIpsWereFound()
4040 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1'));
4141 $this->fallback->expects($this->never())->method('query');
4242
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));
4444 }
4545
4646 public function testFallsBackIfNoIpv4Matches()
4848 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1'));
4949 $this->fallback->expects($this->once())->method('query');
5050
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));
5252 }
5353
5454 public function testReturnsResponseMessageIfIpv6AddressesWereFound()
5656 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('::1'));
5757 $this->fallback->expects($this->never())->method('query');
5858
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));
6060 }
6161
6262 public function testFallsBackIfNoIpv6Matches()
6464 $this->hosts->expects($this->once())->method('getIpsForHost')->willReturn(array('127.0.0.1'));
6565 $this->fallback->expects($this->once())->method('query');
6666
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));
6868 }
6969
7070 public function testDoesReturnReverseIpv4Lookup()
7272 $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array('localhost'));
7373 $this->fallback->expects($this->never())->method('query');
7474
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));
7676 }
7777
7878 public function testFallsBackIfNoReverseIpv4Matches()
8080 $this->hosts->expects($this->once())->method('getHostsForIp')->with('127.0.0.1')->willReturn(array());
8181 $this->fallback->expects($this->once())->method('query');
8282
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));
8484 }
8585
8686 public function testDoesReturnReverseIpv6Lookup()
8888 $this->hosts->expects($this->once())->method('getHostsForIp')->with('2a02:2e0:3fe:100::6')->willReturn(array('ip6-localhost'));
8989 $this->fallback->expects($this->never())->method('query');
9090
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));
9292 }
9393
9494 public function testFallsBackForInvalidAddress()
9696 $this->hosts->expects($this->never())->method('getHostsForIp');
9797 $this->fallback->expects($this->once())->method('query');
9898
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));
100100 }
101101
102102 public function testReverseFallsBackForInvalidIpv4Address()
104104 $this->hosts->expects($this->never())->method('getHostsForIp');
105105 $this->fallback->expects($this->once())->method('query');
106106
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));
108108 }
109109
110110 public function testReverseFallsBackForInvalidLengthIpv6Address()
112112 $this->hosts->expects($this->never())->method('getHostsForIp');
113113 $this->fallback->expects($this->once())->method('query');
114114
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));
116116 }
117117
118118 public function testReverseFallsBackForInvalidHexIpv6Address()
120120 $this->hosts->expects($this->never())->method('getHostsForIp');
121121 $this->fallback->expects($this->once())->method('query');
122122
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));
124124 }
125125 }
+0
-83
tests/Query/RecordBagTest.php less more
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
-193
tests/Query/RecordCacheTest.php less more
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 }
2323 $executor
2424 ->expects($this->once())
2525 ->method('query')
26 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
26 ->with($this->isInstanceOf('React\Dns\Query\Query'))
2727 ->will($this->returnValue($this->expectPromiseOnce()));
2828
2929 $retryExecutor = new RetryExecutor($executor, 2);
3030
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);
3333 }
3434
3535 /**
4444 $executor
4545 ->expects($this->exactly(2))
4646 ->method('query')
47 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
47 ->with($this->isInstanceOf('React\Dns\Query\Query'))
4848 ->will($this->onConsecutiveCalls(
49 $this->returnCallback(function ($domain, $query) {
49 $this->returnCallback(function ($query) {
5050 return Promise\reject(new TimeoutException("timeout"));
5151 }),
52 $this->returnCallback(function ($domain, $query) use ($response) {
52 $this->returnCallback(function ($query) use ($response) {
5353 return Promise\resolve($response);
5454 })
5555 ));
6464
6565 $retryExecutor = new RetryExecutor($executor, 2);
6666
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);
6969 }
7070
7171 /**
7878 $executor
7979 ->expects($this->exactly(3))
8080 ->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) {
8383 return Promise\reject(new TimeoutException("timeout"));
8484 }));
8585
9393
9494 $retryExecutor = new RetryExecutor($executor, 2);
9595
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);
9898 }
9999
100100 /**
107107 $executor
108108 ->expects($this->once())
109109 ->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) {
112112 return Promise\reject(new \Exception);
113113 }));
114114
122122
123123 $retryExecutor = new RetryExecutor($executor, 2);
124124
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);
127127 }
128128
129129 /**
138138 $executor
139139 ->expects($this->once())
140140 ->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) {
143143 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
144144 ++$cancelled;
145145 $reject(new CancellationException('Cancelled'));
151151
152152 $retryExecutor = new RetryExecutor($executor, 2);
153153
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);
156156
157157 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
158158
174174 $executor
175175 ->expects($this->exactly(2))
176176 ->method('query')
177 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
177 ->with($this->isInstanceOf('React\Dns\Query\Query'))
178178 ->will($this->onConsecutiveCalls(
179179 $this->returnValue($deferred->promise()),
180 $this->returnCallback(function ($domain, $query) use (&$cancelled) {
180 $this->returnCallback(function ($query) use (&$cancelled) {
181181 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
182182 ++$cancelled;
183183 $reject(new CancellationException('Cancelled'));
189189
190190 $retryExecutor = new RetryExecutor($executor, 2);
191191
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);
194194
195195 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
196196
216216 $executor
217217 ->expects($this->once())
218218 ->method('query')
219 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
219 ->with($this->isInstanceOf('React\Dns\Query\Query'))
220220 ->willReturn(Promise\resolve($this->createStandardResponse()));
221221
222222 $retryExecutor = new RetryExecutor($executor, 0);
223223
224224 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);
227227
228228 $this->assertEquals(0, gc_collect_cycles());
229229 }
242242 $executor
243243 ->expects($this->any())
244244 ->method('query')
245 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
245 ->with($this->isInstanceOf('React\Dns\Query\Query'))
246246 ->willReturn(Promise\reject(new TimeoutException("timeout")));
247247
248248 $retryExecutor = new RetryExecutor($executor, 0);
249249
250250 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);
253253
254254 $this->assertEquals(0, gc_collect_cycles());
255255 }
272272 $executor
273273 ->expects($this->once())
274274 ->method('query')
275 ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query'))
275 ->with($this->isInstanceOf('React\Dns\Query\Query'))
276276 ->willReturn($deferred->promise());
277277
278278 $retryExecutor = new RetryExecutor($executor, 0);
279279
280280 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);
283283 $promise->cancel();
284284 $promise = null;
285285
300300 $executor
301301 ->expects($this->once())
302302 ->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) {
305305 return Promise\reject(new \Exception);
306306 }));
307307
308308 $retryExecutor = new RetryExecutor($executor, 2);
309309
310310 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);
313313
314314 $this->assertEquals(0, gc_collect_cycles());
315315 }
338338 protected function createStandardResponse()
339339 {
340340 $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);
343343 $response->answers[] = new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131');
344 $response->prepare();
345344
346345 return $response;
347346 }
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 }
2828 $this->wrapped
2929 ->expects($this->once())
3030 ->method('query')
31 ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) {
31 ->will($this->returnCallback(function ($query) use (&$cancelled) {
3232 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
3333 ++$cancelled;
3434 $reject(new CancellationException('Cancelled'));
3737 return $deferred->promise();
3838 }));
3939
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);
4242
4343 $this->assertEquals(0, $cancelled);
4444 $promise->cancel();
5454 ->method('query')
5555 ->willReturn(Promise\resolve('0.0.0.0'));
5656
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);
5959
6060 $promise->then($this->expectCallableOnce(), $this->expectCallableNever());
6161 }
6767 ->method('query')
6868 ->willReturn(Promise\reject(new \RuntimeException()));
6969
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);
7272
7373 $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith(new \RuntimeException()));
7474 }
8282 $this->wrapped
8383 ->expects($this->once())
8484 ->method('query')
85 ->will($this->returnCallback(function ($domain, $query) use (&$cancelled) {
85 ->will($this->returnCallback(function ($query) use (&$cancelled) {
8686 $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) {
8787 ++$cancelled;
8888 $reject(new CancellationException('Cancelled'));
102102 $this->attribute($this->equalTo('DNS query for igor.io timed out'), 'message')
103103 ));
104104
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);
107107
108108 $this->assertEquals(0, $cancelled);
109109
1111
1212 class UdpTransportExecutorTest extends TestCase
1313 {
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
1492 public function testQueryRejectsIfMessageExceedsUdpSize()
1593 {
1694 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
1795 $loop->expects($this->never())->method('addReadStream');
1896
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);
26126
27127 $this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
28128 $promise->then(null, $this->expectCallableOnce());
29129 }
30130
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();
40145
41146 $this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
42147 $promise->then(null, $this->expectCallableOnce());
43148 }
44149
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
64150 public function testQueryKeepsPendingIfServerRejectsNetworkPacket()
65151 {
66152 $loop = Factory::create();
67153
68 $executor = new UdpTransportExecutor($loop);
154 $executor = new UdpTransportExecutor('127.0.0.1:1', $loop);
69155
70156 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
71157
72158 $wait = true;
73 $promise = $executor->query('127.0.0.1:1', $query)->then(
159 $promise = $executor->query($query)->then(
74160 null,
75161 function ($e) use (&$wait) {
76162 $wait = false;
82168 $this->assertTrue($wait);
83169 }
84170
85 public function testQueryKeepsPendingIfServerSendInvalidMessage()
171 public function testQueryKeepsPendingIfServerSendsInvalidMessage()
86172 {
87173 $loop = Factory::create();
88174
93179 });
94180
95181 $address = stream_socket_get_name($server, false);
96 $executor = new UdpTransportExecutor($loop);
182 $executor = new UdpTransportExecutor($address, $loop);
97183
98184 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
99185
100186 $wait = true;
101 $promise = $executor->query($address, $query)->then(
187 $promise = $executor->query($query)->then(
102188 null,
103189 function ($e) use (&$wait) {
104190 $wait = false;
110196 $this->assertTrue($wait);
111197 }
112198
113 public function testQueryKeepsPendingIfServerSendInvalidId()
199 public function testQueryKeepsPendingIfServerSendsInvalidId()
114200 {
115201 $parser = new Parser();
116202 $dumper = new BinaryDumper();
122208 $data = stream_socket_recvfrom($server, 512, 0, $peer);
123209
124210 $message = $parser->parseMessage($data);
125 $message->header->set('id', 0);
211 $message->id = 0;
126212
127213 stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer);
128214 });
129215
130216 $address = stream_socket_get_name($server, false);
131 $executor = new UdpTransportExecutor($loop, $parser, $dumper);
217 $executor = new UdpTransportExecutor($address, $loop);
132218
133219 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
134220
135221 $wait = true;
136 $promise = $executor->query($address, $query)->then(
222 $promise = $executor->query($query)->then(
137223 null,
138224 function ($e) use (&$wait) {
139225 $wait = false;
157243 $data = stream_socket_recvfrom($server, 512, 0, $peer);
158244
159245 $message = $parser->parseMessage($data);
160 $message->header->set('tc', 1);
246 $message->tc = true;
161247
162248 stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer);
163249 });
164250
165251 $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);
186260 }
187261
188262 public function testQueryResolvesIfServerSendsValidResponse()
202276 });
203277
204278 $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);
210284 $response = \Clue\React\Block\await($promise, $loop, 0.2);
211285
212286 $this->assertInstanceOf('React\Dns\Model\Message', $response);
1818 $this->assertInstanceOf('React\Dns\Resolver\Resolver', $resolver);
1919 }
2020
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);
66135 }
67136
68137 /**
69138 * @test
70 * @dataProvider factoryShouldAddDefaultPortProvider
139 * @expectedException InvalidArgumentException
71140 */
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);
93178 }
94179
95180 private function getResolverPrivateExecutor($resolver)
114199 return $reflector->getValue($resolver);
115200 }
116201
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);
120205 $reflector->setAccessible(true);
121206 return $reflector->getValue($resolver);
122207 }
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 }
130208 }
11
22 namespace React\Tests\Dns\Resolver;
33
4 use PHPUnit\Framework\TestCase;
4 use React\Tests\Dns\TestCase;
55 use React\Dns\Resolver\Resolver;
6 use React\Dns\Query\Query;
76 use React\Dns\Model\Message;
87 use React\Dns\Model\Record;
98
109 class ResolveAliasesTest extends TestCase
1110 {
1211 /**
13 * @covers React\Dns\Resolver\Resolver::resolveAliases
14 * @covers React\Dns\Resolver\Resolver::valuesByNameAndType
1512 * @dataProvider provideAliasedAnswers
1613 */
1714 public function testResolveAliases(array $expectedAnswers, array $answers, $name)
1815 {
16 $message = new Message();
17 foreach ($answers as $answer) {
18 $message->answers[] = $answer;
19 }
20
1921 $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));
2123
22 $answers = $resolver->resolveAliases($answers, $name);
24 $resolver = new Resolver($executor);
2325
24 $this->assertEquals($expectedAnswers, $answers);
26 $answers = $resolver->resolveAll($name, Message::TYPE_A);
27
28 $answers->then($this->expectCallableOnceWith($expectedAnswers), null);
2529 }
2630
2731 public function provideAliasedAnswers()
4953 new Record('igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
5054 new Record('foo.igor.io', Message::TYPE_A, Message::CLASS_IN, 3600, '178.79.169.131'),
5155 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),
6056 ),
6157 'igor.io',
6258 ),
1818 $executor
1919 ->expects($this->once())
2020 ->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);
2626 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '178.79.169.131');
2727
2828 return Promise\resolve($response);
2929 }));
3030
31 $resolver = new Resolver('8.8.8.8:53', $executor);
31 $resolver = new Resolver($executor);
3232 $resolver->resolve('igor.io')->then($this->expectCallableOnceWith('178.79.169.131'));
3333 }
3434
3939 $executor
4040 ->expects($this->once())
4141 ->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);
4747 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1');
4848
4949 return Promise\resolve($response);
5050 }));
5151
52 $resolver = new Resolver('8.8.8.8:53', $executor);
52 $resolver = new Resolver($executor);
5353 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1')));
5454 }
5555
6060 $executor
6161 ->expects($this->once())
6262 ->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);
6868 $response->answers[] = new Record($query->name, Message::TYPE_TXT, $query->class, 3600, array('ignored'));
6969 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1');
7070
7171 return Promise\resolve($response);
7272 }));
7373
74 $resolver = new Resolver('8.8.8.8:53', $executor);
74 $resolver = new Resolver($executor);
7575 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1')));
7676 }
7777
8282 $executor
8383 ->expects($this->once())
8484 ->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);
9090 $response->answers[] = new Record($query->name, Message::TYPE_CNAME, $query->class, 3600, 'example.com');
9191 $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::1');
9292 $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);
9998 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(
10099 $this->expectCallableOnceWith($this->equalTo(array('::1', '::2')))
101100 );
108107 $executor
109108 ->expects($this->once())
110109 ->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);
116115 $response->answers[] = new Record('Blog.wyrihaximus.net', $query->type, $query->class, 3600, '178.79.169.131');
117116
118117 return Promise\resolve($response);
119118 }));
120119
121 $resolver = new Resolver('8.8.8.8:53', $executor);
120 $resolver = new Resolver($executor);
122121 $resolver->resolve('blog.wyrihaximus.net')->then($this->expectCallableOnceWith('178.79.169.131'));
123122 }
124123
129128 $executor
130129 ->expects($this->once())
131130 ->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);
137136 $response->answers[] = new Record('foo.bar', $query->type, $query->class, 3600, '178.79.169.131');
138137
139138 return Promise\resolve($response);
141140
142141 $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException'));
143142
144 $resolver = new Resolver('8.8.8.8:53', $executor);
143 $resolver = new Resolver($executor);
145144 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
146145 }
147146
154153 $executor
155154 ->expects($this->once())
156155 ->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);
162161
163162 return Promise\resolve($response);
164163 }));
167166 return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io did not return a valid answer (NOERROR / NODATA)');
168167 }));
169168
170 $resolver = new Resolver('8.8.8.8:53', $executor);
169 $resolver = new Resolver($executor);
171170 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
172171 }
173172
211210 $executor
212211 ->expects($this->once())
213212 ->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);
220219
221220 return Promise\resolve($response);
222221 }));
225224 return ($param instanceof RecordNotFoundException && $param->getCode() === $code && $param->getMessage() === $expectedMessage);
226225 }));
227226
228 $resolver = new Resolver('8.8.8.8:53', $executor);
227 $resolver = new Resolver($executor);
229228 $resolver->resolve('example.com')->then($this->expectCallableNever(), $errback);
230229 }
231230
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
246231 private function createExecutorMock()
247232 {
248233 return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();