Codebase list reactphp-dns / 3d5acd7
Merge branch 'upstream' mirabilos authored 5 years ago mirabilos committed 5 years ago
26 changed file(s) with 1610 addition(s) and 134 deletion(s). Raw diff Collapse all Expand all
00 # Changelog
1
2 ## 0.4.15 (2018-07-02)
3
4 * Feature: Add `resolveAll()` method to support custom query types in `Resolver`.
5 (#110 by @clue and @WyriHaximus)
6
7 ```php
8 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
9 echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
10 });
11 ```
12
13 * Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
14 (#104, #105, #106, #107 and #108 by @clue)
15
16 * Feature: Add support for `Message::TYPE_ANY` parse unknown types as binary data.
17 (#104 by @clue)
18
19 * Feature: Improve error messages for failed queries and improve documentation.
20 (#109 by @clue)
21
22 * Feature: Add reverse DNS lookup example.
23 (#111 by @clue)
24
25 ## 0.4.14 (2018-06-26)
26
27 * Feature: Add `UdpTransportExecutor`, validate incoming DNS response messages
28 to avoid cache poisoning attacks and deprecate legacy `Executor`.
29 (#101 and #103 by @clue)
30
31 * Feature: Forward compatibility with Cache 0.5
32 (#102 by @clue)
33
34 * Deprecate legacy `Query::$currentTime` and binary parser data attributes to clean up and simplify API.
35 (#99 by @clue)
136
237 ## 0.4.13 (2018-02-27)
338
1212 * [Basic usage](#basic-usage)
1313 * [Caching](#caching)
1414 * [Custom cache adapter](#custom-cache-adapter)
15 * [Resolver](#resolver)
16 * [resolve()](#resolve)
17 * [resolveAll()](#resolveall)
1518 * [Advanced usage](#advanced-usage)
19 * [UdpTransportExecutor](#udptransportexecutor)
1620 * [HostsFileExecutor](#hostsfileexecutor)
1721 * [Install](#install)
1822 * [Tests](#tests)
5660 Ideally, this method should thus be executed only once before the loop starts
5761 and not repeatedly while it is running.
5862
59 Pending DNS queries can be cancelled by cancelling its pending promise like so:
60
61 ```php
62 $promise = $resolver->resolve('reactphp.org');
63
64 $promise->cancel();
65 ```
66
6763 But there's more.
6864
6965 ## Caching
114110
115111 See also the wiki for possible [cache implementations](https://github.com/reactphp/react/wiki/Users#cache-implementations).
116112
113 ## Resolver
114
115 ### resolve()
116
117 The `resolve(string $domain): PromiseInterface<string,Exception>` method can be used to
118 resolve the given $domain name to a single IPv4 address (type `A` query).
119
120 ```php
121 $resolver->resolve('reactphp.org')->then(function ($ip) {
122 echo 'IP for reactphp.org is ' . $ip . PHP_EOL;
123 });
124 ```
125
126 This is one of the main methods in this package. It sends a DNS query
127 for the given $domain name to your DNS server and returns a single IP
128 address on success.
129
130 If the DNS server sends a DNS response message that contains more than
131 one IP address for this query, it will randomly pick one of the IP
132 addresses from the response. If you want the full list of IP addresses
133 or want to send a different type of query, you should use the
134 [`resolveAll()`](#resolveall) method instead.
135
136 If the DNS server sends a DNS response message that indicates an error
137 code, this method will reject with a `RecordNotFoundException`. Its
138 message and code can be used to check for the response code.
139
140 If the DNS communication fails and the server does not respond with a
141 valid response message, this message will reject with an `Exception`.
142
143 Pending DNS queries can be cancelled by cancelling its pending promise like so:
144
145 ```php
146 $promise = $resolver->resolve('reactphp.org');
147
148 $promise->cancel();
149 ```
150
151 ### resolveAll()
152
153 The `resolveAll(string $host, int $type): PromiseInterface<array,Exception>` method can be used to
154 resolve all record values for the given $domain name and query $type.
155
156 ```php
157 $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
158 echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
159 });
160
161 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
162 echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
163 });
164 ```
165
166 This is one of the main methods in this package. It sends a DNS query
167 for the given $domain name to your DNS server and returns a list with all
168 record values on success.
169
170 If the DNS server sends a DNS response message that contains one or more
171 records for this query, it will return a list with all record values
172 from the response. You can use the `Message::TYPE_*` constants to control
173 which type of query will be sent. Note that this method always returns a
174 list of record values, but each record value type depends on the query
175 type. For example, it returns the IPv4 addresses for type `A` queries,
176 the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
177 `CNAME` and `PTR` queries and structured data for other queries. See also
178 the `Record` documentation for more details.
179
180 If the DNS server sends a DNS response message that indicates an error
181 code, this method will reject with a `RecordNotFoundException`. Its
182 message and code can be used to check for the response code.
183
184 If the DNS communication fails and the server does not respond with a
185 valid response message, this message will reject with an `Exception`.
186
187 Pending DNS queries can be cancelled by cancelling its pending promise like so:
188
189 ```php
190 $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
191
192 $promise->cancel();
193 ```
194
117195 ## Advanced Usage
118196
119 For more advanced usages one can utilize the `React\Dns\Query\Executor` directly.
197 ### UdpTransportExecutor
198
199 The `UdpTransportExecutor` can be used to
200 send DNS queries over a UDP transport.
201
202 This is the main class that sends a DNS query to your DNS server and is used
203 internally by the `Resolver` for the actual message transport.
204
205 For more advanced usages one can utilize this class directly.
120206 The following example looks up the `IPv6` address for `igor.io`.
121207
122208 ```php
123209 $loop = Factory::create();
124
125 $executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
210 $executor = new UdpTransportExecutor($loop);
126211
127212 $executor->query(
128213 '8.8.8.8:53',
129 new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time())
130 )->done(function (Message $message) {
214 new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
215 )->then(function (Message $message) {
131216 foreach ($message->answers as $answer) {
132217 echo 'IPv6: ' . $answer->data . PHP_EOL;
133218 }
134219 }, 'printf');
135220
136221 $loop->run();
137
138222 ```
139223
140224 See also the [fourth example](examples).
141225
226 Note that this executor does not implement a timeout, so you will very likely
227 want to use this in combination with a `TimeoutExecutor` like this:
228
229 ```php
230 $executor = new TimeoutExecutor(
231 new UdpTransportExecutor($loop),
232 3.0,
233 $loop
234 );
235 ```
236
237 Also note that this executor uses an unreliable UDP transport and that it
238 does not implement any retry logic, so you will likely want to use this in
239 combination with a `RetryExecutor` like this:
240
241 ```php
242 $executor = new RetryExecutor(
243 new TimeoutExecutor(
244 new UdpTransportExecutor($loop),
245 3.0,
246 $loop
247 )
248 );
249 ```
250
251 > Internally, this class uses PHP's UDP sockets and does not take advantage
252 of [react/datagram](https://github.com/reactphp/datagram) purely for
253 organizational reasons to avoid a cyclic dependency between the two
254 packages. Higher-level components should take advantage of the Datagram
255 component instead of reimplementing this socket logic from scratch.
256
142257 ### HostsFileExecutor
143258
144 Note that the above `Executor` class always performs an actual DNS query.
259 Note that the above `UdpTransportExecutor` class always performs an actual DNS query.
145260 If you also want to take entries from your hosts file into account, you may
146261 use this code:
147262
148263 ```php
149264 $hosts = \React\Dns\Config\HostsFile::loadFromPathBlocking();
150265
151 $executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
266 $executor = new UdpTransportExecutor($loop);
152267 $executor = new HostsFileExecutor($hosts, $executor);
153268
154269 $executor->query(
155270 '8.8.8.8:53',
156 new Query('localhost', Message::TYPE_A, Message::CLASS_IN, time())
271 new Query('localhost', Message::TYPE_A, Message::CLASS_IN)
157272 );
158273 ```
159274
165280 This will install the latest supported version:
166281
167282 ```bash
168 $ composer require react/dns:^0.4.13
283 $ composer require react/dns:^0.4.15
169284 ```
170285
171286 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.4.0|~0.3.0",
7 "react/cache": "^0.5 || ^0.4 || ^0.3",
88 "react/event-loop": "^1.0 || ^0.5 || ^0.4 || ^0.3.5",
99 "react/promise": "^2.1 || ^1.2.1",
1010 "react/promise-timer": "^1.2",
+0
-32
examples/04-query-a-and-aaaa.php less more
0 <?php
1
2 use React\Dns\Model\Message;
3 use React\Dns\Protocol\BinaryDumper;
4 use React\Dns\Protocol\Parser;
5 use React\Dns\Query\Executor;
6 use React\Dns\Query\Query;
7 use React\EventLoop\Factory;
8
9 require __DIR__ . '/../vendor/autoload.php';
10
11 $loop = Factory::create();
12
13 $executor = new Executor($loop, new Parser(), new BinaryDumper(), null);
14
15 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
16
17 $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN, time());
18 $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN, time());
19
20 $executor->query('8.8.8.8:53', $ipv4Query)->done(function (Message $message) {
21 foreach ($message->answers as $answer) {
22 echo 'IPv4: ' . $answer->data . PHP_EOL;
23 }
24 }, 'printf');
25 $executor->query('8.8.8.8:53', $ipv6Query)->done(function (Message $message) {
26 foreach ($message->answers as $answer) {
27 echo 'IPv6: ' . $answer->data . PHP_EOL;
28 }
29 }, 'printf');
30
31 $loop->run();
0 <?php
1
2 use React\Dns\Config\Config;
3 use React\Dns\Resolver\Factory;
4 use React\Dns\Model\Message;
5
6 require __DIR__ . '/../vendor/autoload.php';
7
8 $loop = React\EventLoop\Factory::create();
9
10 $config = Config::loadSystemConfigBlocking();
11 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
12
13 $factory = new Factory();
14 $resolver = $factory->create($server, $loop);
15
16 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
17
18 $resolver->resolveAll($name, Message::TYPE_A)->then(function (array $ips) use ($name) {
19 echo 'IPv4 addresses for ' . $name . ': ' . implode(', ', $ips) . PHP_EOL;
20 }, function (Exception $e) use ($name) {
21 echo 'No IPv4 addresses for ' . $name . ': ' . $e->getMessage() . PHP_EOL;
22 });
23
24 $resolver->resolveAll($name, Message::TYPE_AAAA)->then(function (array $ips) use ($name) {
25 echo 'IPv6 addresses for ' . $name . ': ' . implode(', ', $ips) . PHP_EOL;
26 }, function (Exception $e) use ($name) {
27 echo 'No IPv6 addresses for ' . $name . ': ' . $e->getMessage() . PHP_EOL;
28 });
29
30 $loop->run();
0 <?php
1
2 use React\Dns\Config\Config;
3 use React\Dns\Resolver\Factory;
4
5 require __DIR__ . '/../vendor/autoload.php';
6
7 $loop = React\EventLoop\Factory::create();
8
9 $config = Config::loadSystemConfigBlocking();
10 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
11
12 $factory = new Factory();
13 $resolver = $factory->create($server, $loop);
14
15 $name = isset($argv[1]) ? $argv[1] : 'google.com';
16 $type = constant('React\Dns\Model\Message::TYPE_' . (isset($argv[2]) ? $argv[2] : 'TXT'));
17
18 $resolver->resolveAll($name, $type)->then(function (array $values) {
19 var_dump($values);
20 }, function (Exception $e) {
21 echo $e->getMessage() . PHP_EOL;
22 });
23
24 $loop->run();
0 <?php
1
2 use React\Dns\Config\Config;
3 use React\Dns\Resolver\Factory;
4 use React\Dns\Model\Message;
5
6 require __DIR__ . '/../vendor/autoload.php';
7
8 $loop = React\EventLoop\Factory::create();
9
10 $config = Config::loadSystemConfigBlocking();
11 $server = $config->nameservers ? reset($config->nameservers) : '8.8.8.8';
12
13 $factory = new Factory();
14 $resolver = $factory->create($server, $loop);
15
16 $ip = isset($argv[1]) ? $argv[1] : '8.8.8.8';
17
18 if (@inet_pton($ip) === false) {
19 exit('Error: Given argument is not a valid IP' . PHP_EOL);
20 }
21
22 if (strpos($ip, ':') === false) {
23 $name = inet_ntop(strrev(inet_pton($ip))) . '.in-addr.arpa';
24 } else {
25 $name = wordwrap(strrev(bin2hex(inet_pton($ip))), 1, '.', true) . '.ip6.arpa';
26 }
27
28 $resolver->resolveAll($name, Message::TYPE_PTR)->then(function (array $names) {
29 var_dump($names);
30 }, function (Exception $e) {
31 echo $e->getMessage() . PHP_EOL;
32 });
33
34 $loop->run();
0 <?php
1
2 use React\Dns\Model\Message;
3 use React\Dns\Query\Query;
4 use React\Dns\Query\UdpTransportExecutor;
5 use React\EventLoop\Factory;
6
7 require __DIR__ . '/../vendor/autoload.php';
8
9 $loop = Factory::create();
10 $executor = new UdpTransportExecutor($loop);
11
12 $name = isset($argv[1]) ? $argv[1] : 'www.google.com';
13
14 $ipv4Query = new Query($name, Message::TYPE_A, Message::CLASS_IN);
15 $ipv6Query = new Query($name, Message::TYPE_AAAA, Message::CLASS_IN);
16
17 $executor->query('8.8.8.8:53', $ipv4Query)->then(function (Message $message) {
18 foreach ($message->answers as $answer) {
19 echo 'IPv4: ' . $answer->data . PHP_EOL;
20 }
21 }, 'printf');
22 $executor->query('8.8.8.8:53', $ipv6Query)->then(function (Message $message) {
23 foreach ($message->answers as $answer) {
24 echo 'IPv6: ' . $answer->data . PHP_EOL;
25 }
26 }, 'printf');
27
28 $loop->run();
0 <?php
1
2 use React\Dns\Model\Message;
3 use React\Dns\Model\Record;
4 use React\Dns\Query\Query;
5 use React\Dns\Query\UdpTransportExecutor;
6 use React\EventLoop\Factory;
7
8 require __DIR__ . '/../vendor/autoload.php';
9
10 $loop = Factory::create();
11 $executor = new UdpTransportExecutor($loop);
12
13 $name = isset($argv[1]) ? $argv[1] : 'google.com';
14
15 $any = new Query($name, Message::TYPE_ANY, Message::CLASS_IN);
16
17 $executor->query('8.8.8.8:53', $any)->then(function (Message $message) {
18 foreach ($message->answers as $answer) {
19 /* @var $answer Record */
20
21 $data = $answer->data;
22
23 switch ($answer->type) {
24 case Message::TYPE_A:
25 $type = 'A';
26 break;
27 case Message::TYPE_AAAA:
28 $type = 'AAAA';
29 break;
30 case Message::TYPE_NS:
31 $type = 'NS';
32 break;
33 case Message::TYPE_PTR:
34 $type = 'PTR';
35 break;
36 case Message::TYPE_CNAME:
37 $type = 'CNAME';
38 break;
39 case Message::TYPE_TXT:
40 // TXT records can contain a list of (binary) strings for each record.
41 // here, we assume this is printable ASCII and simply concatenate output
42 $type = 'TXT';
43 $data = implode('', $data);
44 break;
45 case Message::TYPE_MX:
46 // MX records contain "priority" and "target", only dump its values here
47 $type = 'MX';
48 $data = implode(' ', $data);
49 break;
50 case Message::TYPE_SRV:
51 // SRV records contains priority, weight, port and target, dump structure here
52 $type = 'SRV';
53 $data = json_encode($data);
54 break;
55 case Message::TYPE_SOA:
56 // SOA records contain structured data, dump structure here
57 $type = 'SOA';
58 $data = json_encode($data);
59 break;
60 default:
61 // unknown type uses HEX format
62 $type = 'Type ' . $answer->type;
63 $data = wordwrap(strtoupper(bin2hex($data)), 2, ' ', true);
64 }
65
66 echo $type . ': ' . $data . PHP_EOL;
67 }
68 }, 'printf');
69
70 $loop->run();
33
44 class HeaderBag
55 {
6 public $data = '';
7
86 public $attributes = array(
97 'qdCount' => 0,
108 'anCount' => 0,
1917 'z' => 0,
2018 'rcode' => Message::RCODE_OK,
2119 );
20
21 /**
22 * @deprecated unused, exists for BC only
23 */
24 public $data = '';
2225
2326 public function get($name)
2427 {
22 namespace React\Dns\Model;
33
44 use React\Dns\Query\Query;
5 use React\Dns\Model\Record;
65
76 class Message
87 {
1413 const TYPE_MX = 15;
1514 const TYPE_TXT = 16;
1615 const TYPE_AAAA = 28;
16 const TYPE_SRV = 33;
17 const TYPE_ANY = 255;
1718
1819 const CLASS_IN = 1;
1920
7273 return $response;
7374 }
7475
76 /**
77 * generates a random 16 bit message ID
78 *
79 * This uses a CSPRNG so that an outside attacker that is sending spoofed
80 * DNS response messages can not guess the message ID to avoid possible
81 * cache poisoning attacks.
82 *
83 * The `random_int()` function is only available on PHP 7+ or when
84 * https://github.com/paragonie/random_compat is installed. As such, using
85 * the latest supported PHP version is highly recommended. This currently
86 * falls back to a less secure random number generator on older PHP versions
87 * in the hope that this system is properly protected against outside
88 * attackers, for example by using one of the common local DNS proxy stubs.
89 *
90 * @return int
91 * @see self::getId()
92 * @codeCoverageIgnore
93 */
7594 private static function generateId()
7695 {
96 if (function_exists('random_int')) {
97 return random_int(0, 0xffff);
98 }
7799 return mt_rand(0, 0xffff);
78100 }
79
80 public $data = '';
81101
82102 public $header;
83103 public $questions = array();
85105 public $authority = array();
86106 public $additional = array();
87107
108 /**
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 */
88116 public $consumed = 0;
89117
90118 public function __construct()
92120 $this->header = new HeaderBag();
93121 }
94122
123 /**
124 * Returns the 16 bit message ID
125 *
126 * The response message ID has to match the request message ID. This allows
127 * the receiver to verify this is the correct response message. An outside
128 * attacker may try to inject fake responses by "guessing" the message ID,
129 * so this should use a proper CSPRNG to avoid possible cache poisoning.
130 *
131 * @return int
132 * @see self::generateId()
133 */
134 public function getId()
135 {
136 return $this->header->get('id');
137 }
138
139 /**
140 * Returns the response code (RCODE)
141 *
142 * @return int see self::RCODE_* constants
143 */
144 public function getResponseCode()
145 {
146 return $this->header->get('rcode');
147 }
148
95149 public function prepare()
96150 {
97151 $this->header->populateCounts($this);
33
44 class Record
55 {
6 /**
7 * @var string hostname without trailing dot, for example "reactphp.org"
8 */
69 public $name;
10
11 /**
12 * @var int see Message::TYPE_* constants (UINT16)
13 */
714 public $type;
15
16 /**
17 * @var int see Message::CLASS_IN constant (UINT16)
18 */
819 public $class;
20
21 /**
22 * @var int maximum TTL in seconds (UINT16)
23 */
924 public $ttl;
25
26 /**
27 * The payload data for this record
28 *
29 * The payload data format depends on the record type. As a rule of thumb,
30 * this library will try to express this in a way that can be consumed
31 * easily without having to worry about DNS internals and its binary transport:
32 *
33 * - A:
34 * IPv4 address string, for example "192.168.1.1".
35 * - AAAA:
36 * IPv6 address string, for example "::1".
37 * - CNAME / PTR / NS:
38 * The hostname without trailing dot, for example "reactphp.org".
39 * - TXT:
40 * List of string values, for example `["v=spf1 include:example.com"]`.
41 * This is commonly a list with only a single string value, but this
42 * technically allows multiple strings (0-255 bytes each) in a single
43 * record. This is rarely used and depending on application you may want
44 * to join these together or handle them separately. Each string can
45 * transport any binary data, its character encoding is not defined (often
46 * ASCII/UTF-8 in practice). [RFC 1464](https://tools.ietf.org/html/rfc1464)
47 * suggests using key-value pairs such as `["name=test","version=1"]`, but
48 * interpretation of this is not enforced and left up to consumers of this
49 * library (used for DNS-SD/Zeroconf and others).
50 * - MX:
51 * Mail server priority (UINT16) and target hostname without trailing dot,
52 * for example `{"priority":10,"target":"mx.example.com"}`.
53 * The payload data uses an associative array with fixed keys "priority"
54 * (also commonly referred to as weight or preference) and "target" (also
55 * referred to as exchange). If a response message contains multiple
56 * records of this type, targets should be sorted by priority (lowest
57 * first) - this is left up to consumers of this library (used for SMTP).
58 * - SRV:
59 * Service priority (UINT16), service weight (UINT16), service port (UINT16)
60 * and target hostname without trailing dot, for example
61 * `{"priority":10,"weight":50,"port":8080,"target":"example.com"}`.
62 * The payload data uses an associative array with fixed keys "priority",
63 * "weight", "port" and "target" (also referred to as name).
64 * The target may be an empty host name string if the service is decidedly
65 * not available. If a response message contains multiple records of this
66 * type, targets should be sorted by priority (lowest first) and selected
67 * randomly according to their weight - this is left up to consumers of
68 * this library, see also [RFC 2782](https://tools.ietf.org/html/rfc2782)
69 * for more details.
70 * - SOA:
71 * Includes master hostname without trailing dot, responsible person email
72 * as hostname without trailing dot and serial, refresh, retry, expire and
73 * minimum times in seconds (UINT32 each), for example:
74 * `{"mname":"ns.example.com","rname":"hostmaster.example.com","serial":
75 * 2018082601,"refresh":3600,"retry":1800,"expire":60000,"minimum":3600}`.
76 * - Any other unknown type:
77 * An opaque binary string containing the RDATA as transported in the DNS
78 * record. For forwards compatibility, you should not rely on this format
79 * for unknown types. Future versions may add support for new types and
80 * this may then parse the payload data appropriately - this will not be
81 * considered a BC break. See the format definition of known types above
82 * for more details.
83 *
84 * @var string|string[]|array
85 */
1086 public $data;
1187
1288 public function __construct($name, $type, $class, $ttl = 0, $data = null)
163163 $consumed += $rdLength;
164164
165165 $rdata = inet_ntop($ip);
166 }
167
168 if (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type) {
166 } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
169167 list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed);
170168
171169 $rdata = implode('.', $bodyLabels);
170 } elseif (Message::TYPE_TXT === $type) {
171 $rdata = array();
172 $remaining = $rdLength;
173 while ($remaining) {
174 $len = ord($message->data[$consumed]);
175 $rdata[] = substr($message->data, $consumed + 1, $len);
176 $consumed += $len + 1;
177 $remaining -= $len + 1;
178 }
179 } elseif (Message::TYPE_MX === $type) {
180 list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2)));
181 list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed + 2);
182
183 $rdata = array(
184 'priority' => $priority,
185 'target' => implode('.', $bodyLabels)
186 );
187 } elseif (Message::TYPE_SRV === $type) {
188 list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6)));
189 list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed + 6);
190
191 $rdata = array(
192 'priority' => $priority,
193 'weight' => $weight,
194 'port' => $port,
195 'target' => implode('.', $bodyLabels)
196 );
197 } elseif (Message::TYPE_SOA === $type) {
198 list($primaryLabels, $consumed) = $this->readLabels($message->data, $consumed);
199 list($mailLabels, $consumed) = $this->readLabels($message->data, $consumed);
200 list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20)));
201 $consumed += 20;
202
203 $rdata = array(
204 'mname' => implode('.', $primaryLabels),
205 'rname' => implode('.', $mailLabels),
206 'serial' => $serial,
207 'refresh' => $refresh,
208 'retry' => $retry,
209 'expire' => $expire,
210 'minimum' => $minimum
211 );
212 } else {
213 // unknown types simply parse rdata as an opaque binary string
214 $rdata = substr($message->data, $consumed, $rdLength);
215 $consumed += $rdLength;
172216 }
173217
174218 $message->consumed = $consumed;
1010 use React\Stream\DuplexResourceStream;
1111 use React\Stream\Stream;
1212
13 /**
14 * @deprecated unused, exists for BC only
15 * @see UdpTransportExecutor
16 */
1317 class Executor implements ExecutorInterface
1418 {
1519 private $loop;
66 public $name;
77 public $type;
88 public $class;
9
10 /**
11 * @deprecated still used internally for BC reasons, should not be used externally.
12 */
913 public $currentTime;
1014
11 public function __construct($name, $type, $class, $currentTime)
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)
1222 {
23 if($currentTime === null) {
24 $currentTime = time();
25 }
26
1327 $this->name = $name;
1428 $this->type = $type;
1529 $this->class = $class;
55 use React\Dns\Model\Message;
66 use React\Dns\Model\Record;
77 use React\Promise;
8 use React\Promise\PromiseInterface;
89
10 /**
11 * Wraps an underlying cache interface and exposes only cached DNS data
12 */
913 class RecordCache
1014 {
1115 private $cache;
1620 $this->cache = $cache;
1721 }
1822
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 */
1930 public function lookup(Query $query)
2031 {
2132 $id = $this->serializeQueryToIdentity($query);
2536 return $this->cache
2637 ->get($id)
2738 ->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 */
2845 $recordBag = unserialize($value);
2946
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.
3050 if (null !== $expiredAt && $expiredAt <= $query->currentTime) {
3151 return Promise\reject();
3252 }
3555 });
3656 }
3757
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 */
3865 public function storeResponseMessage($currentTime, Message $message)
3966 {
4067 foreach ($message->answers as $record) {
4269 }
4370 }
4471
72 /**
73 * Stores a single record from a response message in the cache
74 *
75 * @param int $currentTime
76 * @param Record $record
77 */
4578 public function storeRecord($currentTime, Record $record)
4679 {
4780 $id = $this->serializeRecordToIdentity($record);
5285 ->get($id)
5386 ->then(
5487 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
5594 return unserialize($value);
5695 },
5796 function ($e) {
97 // legacy cache < 0.5 cache miss rejects promise, return empty bag here
5898 return new RecordBag();
5999 }
60100 )
61 ->then(function ($recordBag) use ($id, $currentTime, $record, $cache) {
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
62103 $recordBag->set($currentTime, $record);
63104 $cache->set($id, serialize($recordBag));
64105 });
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 UDP transport.
12 *
13 * This is the main class that sends a DNS query to your DNS server and is used
14 * internally by the `Resolver` for the actual message transport.
15 *
16 * For more advanced usages one can utilize this class directly.
17 * The following example looks up the `IPv6` address for `igor.io`.
18 *
19 * ```php
20 * $loop = Factory::create();
21 * $executor = new UdpTransportExecutor($loop);
22 *
23 * $executor->query(
24 * '8.8.8.8:53',
25 * new Query($name, Message::TYPE_AAAA, Message::CLASS_IN)
26 * )->then(function (Message $message) {
27 * foreach ($message->answers as $answer) {
28 * echo 'IPv6: ' . $answer->data . PHP_EOL;
29 * }
30 * }, 'printf');
31 *
32 * $loop->run();
33 * ```
34 *
35 * See also the [fourth example](examples).
36 *
37 * Note that this executor does not implement a timeout, so you will very likely
38 * want to use this in combination with a `TimeoutExecutor` like this:
39 *
40 * ```php
41 * $executor = new TimeoutExecutor(
42 * new UdpTransportExecutor($loop),
43 * 3.0,
44 * $loop
45 * );
46 * ```
47 *
48 * Also note that this executor uses an unreliable UDP transport and that it
49 * does not implement any retry logic, so you will likely want to use this in
50 * combination with a `RetryExecutor` like this:
51 *
52 * ```php
53 * $executor = new RetryExecutor(
54 * new TimeoutExecutor(
55 * new UdpTransportExecutor($loop),
56 * 3.0,
57 * $loop
58 * )
59 * );
60 * ```
61 *
62 * > Internally, this class uses PHP's UDP sockets and does not take advantage
63 * of [react/datagram](https://github.com/reactphp/datagram) purely for
64 * organizational reasons to avoid a cyclic dependency between the two
65 * packages. Higher-level components should take advantage of the Datagram
66 * component instead of reimplementing this socket logic from scratch.
67 */
68 class UdpTransportExecutor implements ExecutorInterface
69 {
70 private $loop;
71 private $parser;
72 private $dumper;
73
74 /**
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
78 */
79 public function __construct(LoopInterface $loop, Parser $parser = null, BinaryDumper $dumper = null)
80 {
81 if ($parser === null) {
82 $parser = new Parser();
83 }
84 if ($dumper === null) {
85 $dumper = new BinaryDumper();
86 }
87
88 $this->loop = $loop;
89 $this->parser = $parser;
90 $this->dumper = $dumper;
91 }
92
93 public function query($nameserver, Query $query)
94 {
95 $request = Message::createRequestForQuery($query);
96
97 $queryData = $this->dumper->toBinary($request);
98 if (isset($queryData[512])) {
99 return \React\Promise\reject(new \RuntimeException(
100 'DNS query for ' . $query->name . ' failed: Query too large for UDP transport'
101 ));
102 }
103
104 // UDP connections are instant, so try connection without a loop or timeout
105 $socket = @\stream_socket_client("udp://$nameserver", $errno, $errstr, 0);
106 if ($socket === false) {
107 return \React\Promise\reject(new \RuntimeException(
108 'DNS query for ' . $query->name . ' failed: Unable to connect to DNS server (' . $errstr . ')',
109 $errno
110 ));
111 }
112
113 // set socket to non-blocking and immediately try to send (fill write buffer)
114 \stream_set_blocking($socket, false);
115 \fwrite($socket, $queryData);
116
117 $loop = $this->loop;
118 $deferred = new Deferred(function () use ($loop, $socket, $query) {
119 // cancellation should remove socket from loop and close socket
120 $loop->removeReadStream($socket);
121 \fclose($socket);
122
123 throw new CancellationException('DNS query for ' . $query->name . ' has been cancelled');
124 });
125
126 $parser = $this->parser;
127 $loop->addReadStream($socket, function ($socket) use ($loop, $deferred, $query, $parser, $request) {
128 // try to read a single data packet from the DNS server
129 // ignoring any errors, this is uses UDP packets and not a stream of data
130 $data = @\fread($socket, 512);
131
132 try {
133 $response = $parser->parseMessage($data);
134 } catch (\Exception $e) {
135 // ignore and await next if we received an invalid message from remote server
136 // this may as well be a fake response from an attacker (possible DOS)
137 return;
138 }
139
140 // ignore and await next if we received an unexpected response ID
141 // this may as well be a fake response from an attacker (possible cache poisoning)
142 if ($response->getId() !== $request->getId()) {
143 return;
144 }
145
146 // we only react to the first valid message, so remove socket from loop and close
147 $loop->removeReadStream($socket);
148 \fclose($socket);
149
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'));
152 return;
153 }
154
155 $deferred->resolve($response);
156 });
157
158 return $deferred->promise();
159 }
160 }
44 use React\Cache\ArrayCache;
55 use React\Cache\CacheInterface;
66 use React\Dns\Config\HostsFile;
7 use React\Dns\Protocol\Parser;
8 use React\Dns\Protocol\BinaryDumper;
97 use React\Dns\Query\CachedExecutor;
10 use React\Dns\Query\Executor;
118 use React\Dns\Query\ExecutorInterface;
129 use React\Dns\Query\HostsFileExecutor;
1310 use React\Dns\Query\RecordCache;
1411 use React\Dns\Query\RetryExecutor;
1512 use React\Dns\Query\TimeoutExecutor;
13 use React\Dns\Query\UdpTransportExecutor;
1614 use React\EventLoop\LoopInterface;
1715
1816 class Factory
7068 protected function createExecutor(LoopInterface $loop)
7169 {
7270 return new TimeoutExecutor(
73 new Executor($loop, new Parser(), new BinaryDumper(), null),
71 new UdpTransportExecutor($loop),
7472 5.0,
7573 $loop
7674 );
11
22 namespace React\Dns\Resolver;
33
4 use React\Dns\Model\Message;
45 use React\Dns\Query\ExecutorInterface;
56 use React\Dns\Query\Query;
67 use React\Dns\RecordNotFoundException;
7 use React\Dns\Model\Message;
8 use React\Promise\PromiseInterface;
89
910 class Resolver
1011 {
1718 $this->executor = $executor;
1819 }
1920
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 */
2059 public function resolve($domain)
2160 {
22 $query = new Query($domain, Message::TYPE_A, Message::CLASS_IN, time());
61 return $this->resolveAll($domain, Message::TYPE_A)->then(function (array $ips) {
62 return $ips[array_rand($ips)];
63 });
64 }
65
66 /**
67 * Resolves all record values for the given $domain name and query $type.
68 *
69 * ```php
70 * $resolver->resolveAll('reactphp.org', Message::TYPE_A)->then(function ($ips) {
71 * echo 'IPv4 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
72 * });
73 *
74 * $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(function ($ips) {
75 * echo 'IPv6 addresses for reactphp.org ' . implode(', ', $ips) . PHP_EOL;
76 * });
77 * ```
78 *
79 * This is one of the main methods in this package. It sends a DNS query
80 * for the given $domain name to your DNS server and returns a list with all
81 * record values on success.
82 *
83 * If the DNS server sends a DNS response message that contains one or more
84 * records for this query, it will return a list with all record values
85 * from the response. You can use the `Message::TYPE_*` constants to control
86 * which type of query will be sent. Note that this method always returns a
87 * list of record values, but each record value type depends on the query
88 * type. For example, it returns the IPv4 addresses for type `A` queries,
89 * the IPv6 addresses for type `AAAA` queries, the hostname for type `NS`,
90 * `CNAME` and `PTR` queries and structured data for other queries. See also
91 * the `Record` documentation for more details.
92 *
93 * If the DNS server sends a DNS response message that indicates an error
94 * code, this method will reject with a `RecordNotFoundException`. Its
95 * message and code can be used to check for the response code.
96 *
97 * If the DNS communication fails and the server does not respond with a
98 * valid response message, this message will reject with an `Exception`.
99 *
100 * Pending DNS queries can be cancelled by cancelling its pending promise like so:
101 *
102 * ```php
103 * $promise = $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA);
104 *
105 * $promise->cancel();
106 * ```
107 *
108 * @param string $domain
109 * @return PromiseInterface Returns a promise which resolves with all record values on success or
110 * rejects with an Exception on error.
111 */
112 public function resolveAll($domain, $type)
113 {
114 $query = new Query($domain, $type, Message::CLASS_IN);
23115 $that = $this;
24116
25 return $this->executor
26 ->query($this->nameserver, $query)
27 ->then(function (Message $response) use ($query, $that) {
28 return $that->extractAddress($query, $response);
29 });
30 }
31
117 return $this->executor->query(
118 $this->nameserver,
119 $query
120 )->then(function (Message $response) use ($query, $that) {
121 return $that->extractValues($query, $response);
122 });
123 }
124
125 /**
126 * @deprecated unused, exists for BC only
127 */
32128 public function extractAddress(Query $query, Message $response)
33129 {
130 $addresses = $this->extractValues($query, $response);
131
132 return $addresses[array_rand($addresses)];
133 }
134
135 /**
136 * [Internal] extract all resource record values from response for this query
137 *
138 * @param Query $query
139 * @param Message $response
140 * @return array
141 * @throws RecordNotFoundException when response indicates an error or contains no data
142 * @internal
143 */
144 public function extractValues(Query $query, Message $response)
145 {
146 // reject if response code indicates this is an error response message
147 $code = $response->getResponseCode();
148 if ($code !== Message::RCODE_OK) {
149 switch ($code) {
150 case Message::RCODE_FORMAT_ERROR:
151 $message = 'Format Error';
152 break;
153 case Message::RCODE_SERVER_FAILURE:
154 $message = 'Server Failure';
155 break;
156 case Message::RCODE_NAME_ERROR:
157 $message = 'Non-Existent Domain / NXDOMAIN';
158 break;
159 case Message::RCODE_NOT_IMPLEMENTED:
160 $message = 'Not Implemented';
161 break;
162 case Message::RCODE_REFUSED:
163 $message = 'Refused';
164 break;
165 default:
166 $message = 'Unknown error response code ' . $code;
167 }
168 throw new RecordNotFoundException(
169 'DNS query for ' . $query->name . ' returned an error response (' . $message . ')',
170 $code
171 );
172 }
173
34174 $answers = $response->answers;
35
36 $addresses = $this->resolveAliases($answers, $query->name);
37
175 $addresses = $this->valuesByNameAndType($answers, $query->name, $query->type);
176
177 // reject if we did not receive a valid answer (domain is valid, but no record for this type could be found)
38178 if (0 === count($addresses)) {
39 $message = 'DNS Request did not return valid answer.';
40 throw new RecordNotFoundException($message);
41 }
42
43 $address = $addresses[array_rand($addresses)];
44 return $address;
45 }
46
179 throw new RecordNotFoundException(
180 'DNS query for ' . $query->name . ' did not return a valid answer (NOERROR / NODATA)'
181 );
182 }
183
184 return array_values($addresses);
185 }
186
187 /**
188 * @deprecated unused, exists for BC only
189 */
47190 public function resolveAliases(array $answers, $name)
48191 {
192 return $this->valuesByNameAndType($answers, $name, Message::TYPE_A);
193 }
194
195 /**
196 * @param \React\Dns\Model\Record[] $answers
197 * @param string $name
198 * @param int $type
199 * @return array
200 */
201 private function valuesByNameAndType(array $answers, $name, $type)
202 {
203 // return all record values for this name and type (if any)
49204 $named = $this->filterByName($answers, $name);
50 $aRecords = $this->filterByType($named, Message::TYPE_A);
205 $records = $this->filterByType($named, $type);
206 if ($records) {
207 return $this->mapRecordData($records);
208 }
209
210 // no matching records found? check if there are any matching CNAMEs instead
51211 $cnameRecords = $this->filterByType($named, Message::TYPE_CNAME);
52
53 if ($aRecords) {
54 return $this->mapRecordData($aRecords);
55 }
56
57212 if ($cnameRecords) {
58 $aRecords = array();
59
60213 $cnames = $this->mapRecordData($cnameRecords);
61214 foreach ($cnames as $cname) {
62 $targets = $this->filterByName($answers, $cname);
63 $aRecords = array_merge(
64 $aRecords,
65 $this->resolveAliases($answers, $cname)
215 $records = array_merge(
216 $records,
217 $this->valuesByNameAndType($answers, $cname, $type)
66218 );
67219 }
68
69 return $aRecords;
70 }
71
72 return array();
220 }
221
222 return $records;
73223 }
74224
75225 private function filterByName(array $answers, $name)
11
22 namespace React\Tests\Dns;
33
4 use React\Tests\Dns\TestCase;
54 use React\EventLoop\Factory as LoopFactory;
6 use React\Dns\Resolver\Resolver;
75 use React\Dns\Resolver\Factory;
6 use React\Dns\RecordNotFoundException;
7 use React\Dns\Model\Message;
88
99 class FunctionalTest extends TestCase
1010 {
2020 {
2121 $promise = $this->resolver->resolve('localhost');
2222 $promise->then($this->expectCallableOnce(), $this->expectCallableNever());
23
24 $this->loop->run();
25 }
26
27 public function testResolveAllLocalhostResolvesWithArray()
28 {
29 $promise = $this->resolver->resolveAll('localhost', Message::TYPE_A);
30 $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever());
2331
2432 $this->loop->run();
2533 }
4048 */
4149 public function testResolveInvalidRejects()
4250 {
51 $ex = $this->callback(function ($param) {
52 return ($param instanceof RecordNotFoundException && $param->getCode() === Message::RCODE_NAME_ERROR);
53 });
54
4355 $promise = $this->resolver->resolve('example.invalid');
44 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
56 $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex));
4557
4658 $this->loop->run();
4759 }
4860
4961 public function testResolveCancelledRejectsImmediately()
5062 {
63 $ex = $this->callback(function ($param) {
64 return ($param instanceof \RuntimeException && $param->getMessage() === 'DNS query for google.com has been cancelled');
65 });
66
5167 $promise = $this->resolver->resolve('google.com');
52 $promise->then($this->expectCallableNever(), $this->expectCallableOnce());
68 $promise->then($this->expectCallableNever(), $this->expectCallableOnceWith($ex));
5369 $promise->cancel();
5470
5571 $time = microtime(true);
2525 $this->assertFalse($request->header->isQuery());
2626 $this->assertTrue($request->header->isResponse());
2727 $this->assertEquals(0, $request->header->get('anCount'));
28 $this->assertEquals(Message::RCODE_OK, $request->getResponseCode());
2829 }
2930 }
156156 $this->assertSame('178.79.169.131', $response->answers[0]->data);
157157 }
158158
159 public function testParseAnswerWithUnknownType()
160 {
161 $data = "";
162 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
163 $data .= "23 28 00 01"; // answer: type 9000, class IN
164 $data .= "00 01 51 80"; // answer: ttl 86400
165 $data .= "00 05"; // answer: rdlength 5
166 $data .= "68 65 6c 6c 6f"; // answer: rdata "hello"
167
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);
175
176 $this->assertCount(1, $response->answers);
177 $this->assertSame('igor.io', $response->answers[0]->name);
178 $this->assertSame(9000, $response->answers[0]->type);
179 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
180 $this->assertSame(86400, $response->answers[0]->ttl);
181 $this->assertSame('hello', $response->answers[0]->data);
182 }
183
159184 public function testParseResponseWithCnameAndOffsetPointers()
160185 {
161186 $data = "";
230255 $this->assertSame('2a00:1450:4009:809::200e', $response->answers[0]->data);
231256 }
232257
258 public function testParseTXTResponse()
259 {
260 $data = "";
261 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
262 $data .= "00 10 00 01"; // answer: type TXT, class IN
263 $data .= "00 01 51 80"; // answer: ttl 86400
264 $data .= "00 06"; // answer: rdlength 6
265 $data .= "05 68 65 6c 6c 6f"; // answer: rdata length 5: hello
266
267 $data = $this->convertTcpDumpToBinary($data);
268
269 $response = new Message();
270 $response->header->set('anCount', 1);
271 $response->data = $data;
272
273 $this->parser->parseAnswer($response);
274
275 $this->assertCount(1, $response->answers);
276 $this->assertSame('igor.io', $response->answers[0]->name);
277 $this->assertSame(Message::TYPE_TXT, $response->answers[0]->type);
278 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
279 $this->assertSame(86400, $response->answers[0]->ttl);
280 $this->assertSame(array('hello'), $response->answers[0]->data);
281 }
282
283 public function testParseTXTResponseMultiple()
284 {
285 $data = "";
286 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
287 $data .= "00 10 00 01"; // answer: type TXT, class IN
288 $data .= "00 01 51 80"; // answer: ttl 86400
289 $data .= "00 0C"; // answer: rdlength 12
290 $data .= "05 68 65 6c 6c 6f 05 77 6f 72 6c 64"; // answer: rdata length 5: hello, length 5: world
291
292 $data = $this->convertTcpDumpToBinary($data);
293
294 $response = new Message();
295 $response->header->set('anCount', 1);
296 $response->data = $data;
297
298 $this->parser->parseAnswer($response);
299
300 $this->assertCount(1, $response->answers);
301 $this->assertSame('igor.io', $response->answers[0]->name);
302 $this->assertSame(Message::TYPE_TXT, $response->answers[0]->type);
303 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
304 $this->assertSame(86400, $response->answers[0]->ttl);
305 $this->assertSame(array('hello', 'world'), $response->answers[0]->data);
306 }
307
308 public function testParseMXResponse()
309 {
310 $data = "";
311 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
312 $data .= "00 0f 00 01"; // answer: type MX, class IN
313 $data .= "00 01 51 80"; // answer: ttl 86400
314 $data .= "00 09"; // answer: rdlength 9
315 $data .= "00 0a 05 68 65 6c 6c 6f 00"; // answer: rdata priority 10: hello
316
317 $data = $this->convertTcpDumpToBinary($data);
318
319 $response = new Message();
320 $response->header->set('anCount', 1);
321 $response->data = $data;
322
323 $this->parser->parseAnswer($response);
324
325 $this->assertCount(1, $response->answers);
326 $this->assertSame('igor.io', $response->answers[0]->name);
327 $this->assertSame(Message::TYPE_MX, $response->answers[0]->type);
328 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
329 $this->assertSame(86400, $response->answers[0]->ttl);
330 $this->assertSame(array('priority' => 10, 'target' => 'hello'), $response->answers[0]->data);
331 }
332
333 public function testParseSRVResponse()
334 {
335 $data = "";
336 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
337 $data .= "00 21 00 01"; // answer: type SRV, class IN
338 $data .= "00 01 51 80"; // answer: ttl 86400
339 $data .= "00 0C"; // answer: rdlength 12
340 $data .= "00 0a 00 14 1F 90 04 74 65 73 74 00"; // answer: rdata priority 10, weight 20, port 8080 test
341
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);
349
350 $this->assertCount(1, $response->answers);
351 $this->assertSame('igor.io', $response->answers[0]->name);
352 $this->assertSame(Message::TYPE_SRV, $response->answers[0]->type);
353 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
354 $this->assertSame(86400, $response->answers[0]->ttl);
355 $this->assertSame(
356 array(
357 'priority' => 10,
358 'weight' => 20,
359 'port' => 8080,
360 'target' => 'test'
361 ),
362 $response->answers[0]->data
363 );
364 }
365
233366 public function testParseResponseWithTwoAnswers()
234367 {
235368 $data = "";
272405 $this->assertSame('193.223.78.152', $response->answers[1]->data);
273406 }
274407
408 public function testParseNSResponse()
409 {
410 $data = "";
411 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
412 $data .= "00 02 00 01"; // answer: type NS, class IN
413 $data .= "00 01 51 80"; // answer: ttl 86400
414 $data .= "00 07"; // answer: rdlength 7
415 $data .= "05 68 65 6c 6c 6f 00"; // answer: rdata hello
416
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);
424
425 $this->assertCount(1, $response->answers);
426 $this->assertSame('igor.io', $response->answers[0]->name);
427 $this->assertSame(Message::TYPE_NS, $response->answers[0]->type);
428 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
429 $this->assertSame(86400, $response->answers[0]->ttl);
430 $this->assertSame('hello', $response->answers[0]->data);
431 }
432
433 public function testParseSOAResponse()
434 {
435 $data = "";
436 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
437 $data .= "00 06 00 01"; // answer: type SOA, class IN
438 $data .= "00 01 51 80"; // answer: ttl 86400
439 $data .= "00 07"; // answer: rdlength 7
440 $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname)
441 $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname)
442 $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600
443 $data .= "00 09 3a 80 00 00 0e 10"; // answer: 605800, 3600
444
445 $data = $this->convertTcpDumpToBinary($data);
446
447 $response = new Message();
448 $response->header->set('anCount', 1);
449 $response->data = $data;
450
451 $this->parser->parseAnswer($response);
452
453 $this->assertCount(1, $response->answers);
454 $this->assertSame('igor.io', $response->answers[0]->name);
455 $this->assertSame(Message::TYPE_SOA, $response->answers[0]->type);
456 $this->assertSame(Message::CLASS_IN, $response->answers[0]->class);
457 $this->assertSame(86400, $response->answers[0]->ttl);
458 $this->assertSame(
459 array(
460 'mname' => 'ns.hello',
461 'rname' => 'e.hello',
462 'serial' => 2018060501,
463 'refresh' => 10800,
464 'retry' => 3600,
465 'expire' => 604800,
466 'minimum' => 3600
467 ),
468 $response->answers[0]->data
469 );
470 }
471
275472 public function testParsePTRResponse()
276473 {
277474 $data = "";
88 use React\Dns\Query\RecordCache;
99 use React\Dns\Query\Query;
1010 use React\Promise\PromiseInterface;
11 use React\Promise\Promise;
1112
1213 class RecordCacheTest extends TestCase
1314 {
1415 /**
15 * @covers React\Dns\Query\RecordCache
16 * @test
17 */
18 public function lookupOnEmptyCacheShouldReturnNull()
16 * @covers React\Dns\Query\RecordCache
17 * @test
18 */
19 public function lookupOnNewCacheMissShouldReturnNull()
1920 {
2021 $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451);
2122
22 $cache = new RecordCache(new ArrayCache());
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);
2327 $promise = $cache->lookup($query);
2428
2529 $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'));
2696 }
2797
2898 /**
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\UdpTransportExecutor;
9 use React\EventLoop\Factory;
10 use React\Tests\Dns\TestCase;
11
12 class UdpTransportExecutorTest extends TestCase
13 {
14 public function testQueryRejectsIfMessageExceedsUdpSize()
15 {
16 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
17 $loop->expects($this->never())->method('addReadStream');
18
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);
26
27 $this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
28 $promise->then(null, $this->expectCallableOnce());
29 }
30
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);
40
41 $this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
42 $promise->then(null, $this->expectCallableOnce());
43 }
44
45 /**
46 * @group internet
47 */
48 public function testQueryRejectsOnCancellation()
49 {
50 $loop = $this->getMockBuilder('React\EventLoop\LoopInterface')->getMock();
51 $loop->expects($this->once())->method('addReadStream');
52 $loop->expects($this->once())->method('removeReadStream');
53
54 $executor = new UdpTransportExecutor($loop);
55
56 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
57 $promise = $executor->query('8.8.8.8:53', $query);
58 $promise->cancel();
59
60 $this->assertInstanceOf('React\Promise\PromiseInterface', $promise);
61 $promise->then(null, $this->expectCallableOnce());
62 }
63
64 public function testQueryKeepsPendingIfServerRejectsNetworkPacket()
65 {
66 $loop = Factory::create();
67
68 $executor = new UdpTransportExecutor($loop);
69
70 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
71
72 $wait = true;
73 $promise = $executor->query('127.0.0.1:1', $query)->then(
74 null,
75 function ($e) use (&$wait) {
76 $wait = false;
77 throw $e;
78 }
79 );
80
81 \Clue\React\Block\sleep(0.2, $loop);
82 $this->assertTrue($wait);
83 }
84
85 public function testQueryKeepsPendingIfServerSendInvalidMessage()
86 {
87 $loop = Factory::create();
88
89 $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
90 $loop->addReadStream($server, function ($server) {
91 $data = stream_socket_recvfrom($server, 512, 0, $peer);
92 stream_socket_sendto($server, 'invalid', 0, $peer);
93 });
94
95 $address = stream_socket_get_name($server, false);
96 $executor = new UdpTransportExecutor($loop);
97
98 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
99
100 $wait = true;
101 $promise = $executor->query($address, $query)->then(
102 null,
103 function ($e) use (&$wait) {
104 $wait = false;
105 throw $e;
106 }
107 );
108
109 \Clue\React\Block\sleep(0.2, $loop);
110 $this->assertTrue($wait);
111 }
112
113 public function testQueryKeepsPendingIfServerSendInvalidId()
114 {
115 $parser = new Parser();
116 $dumper = new BinaryDumper();
117
118 $loop = Factory::create();
119
120 $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
121 $loop->addReadStream($server, function ($server) use ($parser, $dumper) {
122 $data = stream_socket_recvfrom($server, 512, 0, $peer);
123
124 $message = $parser->parseMessage($data);
125 $message->header->set('id', 0);
126
127 stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer);
128 });
129
130 $address = stream_socket_get_name($server, false);
131 $executor = new UdpTransportExecutor($loop, $parser, $dumper);
132
133 $query = new Query('google.com', Message::TYPE_A, Message::CLASS_IN);
134
135 $wait = true;
136 $promise = $executor->query($address, $query)->then(
137 null,
138 function ($e) use (&$wait) {
139 $wait = false;
140 throw $e;
141 }
142 );
143
144 \Clue\React\Block\sleep(0.2, $loop);
145 $this->assertTrue($wait);
146 }
147
148 public function testQueryRejectsIfServerSendsTruncatedResponse()
149 {
150 $parser = new Parser();
151 $dumper = new BinaryDumper();
152
153 $loop = Factory::create();
154
155 $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
156 $loop->addReadStream($server, function ($server) use ($parser, $dumper) {
157 $data = stream_socket_recvfrom($server, 512, 0, $peer);
158
159 $message = $parser->parseMessage($data);
160 $message->header->set('tc', 1);
161
162 stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer);
163 });
164
165 $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);
186 }
187
188 public function testQueryResolvesIfServerSendsValidResponse()
189 {
190 $parser = new Parser();
191 $dumper = new BinaryDumper();
192
193 $loop = Factory::create();
194
195 $server = stream_socket_server('udp://127.0.0.1:0', $errno, $errstr, STREAM_SERVER_BIND);
196 $loop->addReadStream($server, function ($server) use ($parser, $dumper) {
197 $data = stream_socket_recvfrom($server, 512, 0, $peer);
198
199 $message = $parser->parseMessage($data);
200
201 stream_socket_sendto($server, $dumper->toBinary($message), 0, $peer);
202 });
203
204 $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);
210 $response = \Clue\React\Block\await($promise, $loop, 0.2);
211
212 $this->assertInstanceOf('React\Dns\Model\Message', $response);
213 }
214 }
1111 {
1212 /**
1313 * @covers React\Dns\Resolver\Resolver::resolveAliases
14 * @covers React\Dns\Resolver\Resolver::valuesByNameAndType
1415 * @dataProvider provideAliasedAnswers
1516 */
1617 public function testResolveAliases(array $expectedAnswers, array $answers, $name)
77 use React\Dns\Model\Record;
88 use React\Promise;
99 use React\Tests\Dns\TestCase;
10 use React\Dns\RecordNotFoundException;
1011
1112 class ResolverTest extends TestCase
1213 {
3233 }
3334
3435 /** @test */
36 public function resolveAllShouldQueryGivenRecords()
37 {
38 $executor = $this->createExecutorMock();
39 $executor
40 ->expects($this->once())
41 ->method('query')
42 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
43 ->will($this->returnCallback(function ($nameserver, $query) {
44 $response = new Message();
45 $response->header->set('qr', 1);
46 $response->questions[] = new Record($query->name, $query->type, $query->class);
47 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1');
48
49 return Promise\resolve($response);
50 }));
51
52 $resolver = new Resolver('8.8.8.8:53', $executor);
53 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1')));
54 }
55
56 /** @test */
57 public function resolveAllShouldIgnoreRecordsWithOtherTypes()
58 {
59 $executor = $this->createExecutorMock();
60 $executor
61 ->expects($this->once())
62 ->method('query')
63 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
64 ->will($this->returnCallback(function ($nameserver, $query) {
65 $response = new Message();
66 $response->header->set('qr', 1);
67 $response->questions[] = new Record($query->name, $query->type, $query->class);
68 $response->answers[] = new Record($query->name, Message::TYPE_TXT, $query->class, 3600, array('ignored'));
69 $response->answers[] = new Record($query->name, $query->type, $query->class, 3600, '::1');
70
71 return Promise\resolve($response);
72 }));
73
74 $resolver = new Resolver('8.8.8.8:53', $executor);
75 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then($this->expectCallableOnceWith(array('::1')));
76 }
77
78 /** @test */
79 public function resolveAllShouldReturnMultipleValuesForAlias()
80 {
81 $executor = $this->createExecutorMock();
82 $executor
83 ->expects($this->once())
84 ->method('query')
85 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
86 ->will($this->returnCallback(function ($nameserver, $query) {
87 $response = new Message();
88 $response->header->set('qr', 1);
89 $response->questions[] = new Record($query->name, $query->type, $query->class);
90 $response->answers[] = new Record($query->name, Message::TYPE_CNAME, $query->class, 3600, 'example.com');
91 $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::1');
92 $response->answers[] = new Record('example.com', $query->type, $query->class, 3600, '::2');
93 $response->prepare();
94
95 return Promise\resolve($response);
96 }));
97
98 $resolver = new Resolver('8.8.8.8:53', $executor);
99 $resolver->resolveAll('reactphp.org', Message::TYPE_AAAA)->then(
100 $this->expectCallableOnceWith($this->equalTo(array('::1', '::2')))
101 );
102 }
103
104 /** @test */
35105 public function resolveShouldQueryARecordsAndIgnoreCase()
36106 {
37107 $executor = $this->createExecutorMock();
65135 $response->header->set('qr', 1);
66136 $response->questions[] = new Record($query->name, $query->type, $query->class);
67137 $response->answers[] = new Record('foo.bar', $query->type, $query->class, 3600, '178.79.169.131');
68
69 return Promise\resolve($response);
70 }));
71
72 $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException'));
73
74 $resolver = new Resolver('8.8.8.8:53', $executor);
75 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
76 }
77
78 /** @test */
79 public function resolveWithNoAnswersShouldThrowException()
80 {
81 $executor = $this->createExecutorMock();
82 $executor
83 ->expects($this->once())
84 ->method('query')
85 ->with($this->anything(), $this->isInstanceOf('React\Dns\Query\Query'))
86 ->will($this->returnCallback(function ($nameserver, $query) {
87 $response = new Message();
88 $response->header->set('qr', 1);
89 $response->questions[] = new Record($query->name, $query->type, $query->class);
90138
91139 return Promise\resolve($response);
92140 }));
115163 return Promise\resolve($response);
116164 }));
117165
118 $errback = $this->expectCallableOnceWith($this->isInstanceOf('React\Dns\RecordNotFoundException'));
166 $errback = $this->expectCallableOnceWith($this->callback(function ($param) {
167 return ($param instanceof RecordNotFoundException && $param->getCode() === 0 && $param->getMessage() === 'DNS query for igor.io did not return a valid answer (NOERROR / NODATA)');
168 }));
119169
120170 $resolver = new Resolver('8.8.8.8:53', $executor);
121171 $resolver->resolve('igor.io')->then($this->expectCallableNever(), $errback);
122172 }
123173
174 public function provideRcodeErrors()
175 {
176 return array(
177 array(
178 Message::RCODE_FORMAT_ERROR,
179 'DNS query for example.com returned an error response (Format Error)',
180 ),
181 array(
182 Message::RCODE_SERVER_FAILURE,
183 'DNS query for example.com returned an error response (Server Failure)',
184 ),
185 array(
186 Message::RCODE_NAME_ERROR,
187 'DNS query for example.com returned an error response (Non-Existent Domain / NXDOMAIN)'
188 ),
189 array(
190 Message::RCODE_NOT_IMPLEMENTED,
191 'DNS query for example.com returned an error response (Not Implemented)'
192 ),
193 array(
194 Message::RCODE_REFUSED,
195 'DNS query for example.com returned an error response (Refused)'
196 ),
197 array(
198 99,
199 'DNS query for example.com returned an error response (Unknown error response code 99)'
200 )
201 );
202 }
203
204 /**
205 * @test
206 * @dataProvider provideRcodeErrors
207 */
208 public function resolveWithRcodeErrorShouldCallErrbackIfGiven($code, $expectedMessage)
209 {
210 $executor = $this->createExecutorMock();
211 $executor
212 ->expects($this->once())
213 ->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);
220
221 return Promise\resolve($response);
222 }));
223
224 $errback = $this->expectCallableOnceWith($this->callback(function ($param) use ($code, $expectedMessage) {
225 return ($param instanceof RecordNotFoundException && $param->getCode() === $code && $param->getMessage() === $expectedMessage);
226 }));
227
228 $resolver = new Resolver('8.8.8.8:53', $executor);
229 $resolver->resolve('example.com')->then($this->expectCallableNever(), $errback);
230 }
231
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
124246 private function createExecutorMock()
125247 {
126248 return $this->getMockBuilder('React\Dns\Query\ExecutorInterface')->getMock();