New upstream version 0.4.16
Dominik George
4 years ago
0 | 0 | # Changelog |
1 | ||
2 | ## 0.4.16 (2018-11-11) | |
3 | ||
4 | * Feature: Improve promise cancellation for DNS lookup retries and clean up any garbage references. | |
5 | (#118 by @clue) | |
6 | ||
7 | * Fix: Reject parsing malformed DNS response messages such as incomplete DNS response messages, | |
8 | malformed record data or malformed compressed domain name labels. | |
9 | (#115 and #117 by @clue) | |
10 | ||
11 | * Fix: Fix interpretation of TTL as UINT32 with most significant bit unset. | |
12 | (#116 by @clue) | |
13 | ||
14 | * Fix: Fix caching advanced MX/SRV/TXT/SOA structures. | |
15 | (#112 by @clue) | |
1 | 16 | |
2 | 17 | ## 0.4.15 (2018-07-02) |
3 | 18 | |
13 | 28 | * Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records. |
14 | 29 | (#104, #105, #106, #107 and #108 by @clue) |
15 | 30 | |
16 | * Feature: Add support for `Message::TYPE_ANY` parse unknown types as binary data. | |
31 | * Feature: Add support for `Message::TYPE_ANY` and parse unknown types as binary data. | |
17 | 32 | (#104 by @clue) |
18 | 33 | |
19 | 34 | * Feature: Improve error messages for failed queries and improve documentation. |
280 | 280 | This will install the latest supported version: |
281 | 281 | |
282 | 282 | ```bash |
283 | $ composer require react/dns:^0.4.15 | |
283 | $ composer require react/dns:^0.4.16 | |
284 | 284 | ``` |
285 | 285 | |
286 | 286 | See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades. |
19 | 19 | public $class; |
20 | 20 | |
21 | 21 | /** |
22 | * @var int maximum TTL in seconds (UINT16) | |
22 | * @var int maximum TTL in seconds (UINT32, most significant bit always unset) | |
23 | * @link https://tools.ietf.org/html/rfc2181#section-8 | |
23 | 24 | */ |
24 | 25 | public $ttl; |
25 | 26 |
31 | 31 | |
32 | 32 | /** |
33 | 33 | * @deprecated unused, exists for BC only |
34 | * @codeCoverageIgnore | |
34 | 35 | */ |
35 | 36 | public function parseChunk($data, Message $message) |
36 | 37 | { |
64 | 65 | |
65 | 66 | public function parseHeader(Message $message) |
66 | 67 | { |
67 | if (strlen($message->data) < 12) { | |
68 | if (!isset($message->data[12 - 1])) { | |
68 | 69 | return; |
69 | 70 | } |
70 | 71 | |
95 | 96 | |
96 | 97 | public function parseQuestion(Message $message) |
97 | 98 | { |
98 | if (strlen($message->data) < 2) { | |
99 | return; | |
100 | } | |
101 | ||
102 | 99 | $consumed = $message->consumed; |
103 | 100 | |
104 | 101 | list($labels, $consumed) = $this->readLabels($message->data, $consumed); |
105 | 102 | |
106 | if (null === $labels) { | |
107 | return; | |
108 | } | |
109 | ||
110 | if (strlen($message->data) - $consumed < 4) { | |
103 | if ($labels === null || !isset($message->data[$consumed + 4 - 1])) { | |
111 | 104 | return; |
112 | 105 | } |
113 | 106 | |
131 | 124 | |
132 | 125 | public function parseAnswer(Message $message) |
133 | 126 | { |
134 | if (strlen($message->data) < 2) { | |
135 | return; | |
136 | } | |
137 | ||
138 | 127 | $consumed = $message->consumed; |
139 | 128 | |
140 | list($labels, $consumed) = $this->readLabels($message->data, $consumed); | |
141 | ||
142 | if (null === $labels) { | |
143 | return; | |
144 | } | |
145 | ||
146 | if (strlen($message->data) - $consumed < 10) { | |
129 | list($name, $consumed) = $this->readDomain($message->data, $consumed); | |
130 | ||
131 | if ($name === null || !isset($message->data[$consumed + 10 - 1])) { | |
147 | 132 | return; |
148 | 133 | } |
149 | 134 | |
153 | 138 | list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4))); |
154 | 139 | $consumed += 4; |
155 | 140 | |
141 | // TTL is a UINT32 that must not have most significant bit set for BC reasons | |
142 | if ($ttl < 0 || $ttl >= 1 << 31) { | |
143 | $ttl = 0; | |
144 | } | |
145 | ||
156 | 146 | list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2))); |
157 | 147 | $consumed += 2; |
158 | 148 | |
149 | if (!isset($message->data[$consumed + $rdLength - 1])) { | |
150 | return; | |
151 | } | |
152 | ||
159 | 153 | $rdata = null; |
160 | ||
161 | if (Message::TYPE_A === $type || Message::TYPE_AAAA === $type) { | |
162 | $ip = substr($message->data, $consumed, $rdLength); | |
163 | $consumed += $rdLength; | |
164 | ||
165 | $rdata = inet_ntop($ip); | |
154 | $expected = $consumed + $rdLength; | |
155 | ||
156 | if (Message::TYPE_A === $type) { | |
157 | if ($rdLength === 4) { | |
158 | $rdata = inet_ntop(substr($message->data, $consumed, $rdLength)); | |
159 | $consumed += $rdLength; | |
160 | } | |
161 | } elseif (Message::TYPE_AAAA === $type) { | |
162 | if ($rdLength === 16) { | |
163 | $rdata = inet_ntop(substr($message->data, $consumed, $rdLength)); | |
164 | $consumed += $rdLength; | |
165 | } | |
166 | 166 | } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) { |
167 | list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); | |
168 | ||
169 | $rdata = implode('.', $bodyLabels); | |
167 | list($rdata, $consumed) = $this->readDomain($message->data, $consumed); | |
170 | 168 | } elseif (Message::TYPE_TXT === $type) { |
171 | 169 | $rdata = array(); |
172 | $remaining = $rdLength; | |
173 | while ($remaining) { | |
170 | while ($consumed < $expected) { | |
174 | 171 | $len = ord($message->data[$consumed]); |
175 | $rdata[] = substr($message->data, $consumed + 1, $len); | |
172 | $rdata[] = (string)substr($message->data, $consumed + 1, $len); | |
176 | 173 | $consumed += $len + 1; |
177 | $remaining -= $len + 1; | |
178 | 174 | } |
179 | 175 | } 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 | ); | |
176 | if ($rdLength > 2) { | |
177 | list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2))); | |
178 | list($target, $consumed) = $this->readDomain($message->data, $consumed + 2); | |
179 | ||
180 | $rdata = array( | |
181 | 'priority' => $priority, | |
182 | 'target' => $target | |
183 | ); | |
184 | } | |
187 | 185 | } 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 | ); | |
186 | if ($rdLength > 6) { | |
187 | list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6))); | |
188 | list($target, $consumed) = $this->readDomain($message->data, $consumed + 6); | |
189 | ||
190 | $rdata = array( | |
191 | 'priority' => $priority, | |
192 | 'weight' => $weight, | |
193 | 'port' => $port, | |
194 | 'target' => $target | |
195 | ); | |
196 | } | |
197 | 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 | ); | |
198 | list($mname, $consumed) = $this->readDomain($message->data, $consumed); | |
199 | list($rname, $consumed) = $this->readDomain($message->data, $consumed); | |
200 | ||
201 | if ($mname !== null && $rname !== null && isset($message->data[$consumed + 20 - 1])) { | |
202 | list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20))); | |
203 | $consumed += 20; | |
204 | ||
205 | $rdata = array( | |
206 | 'mname' => $mname, | |
207 | 'rname' => $rname, | |
208 | 'serial' => $serial, | |
209 | 'refresh' => $refresh, | |
210 | 'retry' => $retry, | |
211 | 'expire' => $expire, | |
212 | 'minimum' => $minimum | |
213 | ); | |
214 | } | |
212 | 215 | } else { |
213 | 216 | // unknown types simply parse rdata as an opaque binary string |
214 | 217 | $rdata = substr($message->data, $consumed, $rdLength); |
215 | 218 | $consumed += $rdLength; |
216 | 219 | } |
217 | 220 | |
221 | // ensure parsing record data consumes expact number of bytes indicated in record length | |
222 | if ($consumed !== $expected || $rdata === null) { | |
223 | return; | |
224 | } | |
225 | ||
218 | 226 | $message->consumed = $consumed; |
219 | 227 | |
220 | $name = implode('.', $labels); | |
221 | $ttl = $this->signedLongToUnsignedLong($ttl); | |
222 | 228 | $record = new Record($name, $type, $class, $ttl, $rdata); |
223 | 229 | |
224 | 230 | $message->answers[] = $record; |
230 | 236 | return $message; |
231 | 237 | } |
232 | 238 | |
239 | private function readDomain($data, $consumed) | |
240 | { | |
241 | list ($labels, $consumed) = $this->readLabels($data, $consumed); | |
242 | ||
243 | if ($labels === null) { | |
244 | return array(null, null); | |
245 | } | |
246 | ||
247 | return array(implode('.', $labels), $consumed); | |
248 | } | |
249 | ||
233 | 250 | private function readLabels($data, $consumed) |
234 | 251 | { |
235 | 252 | $labels = array(); |
236 | 253 | |
237 | 254 | while (true) { |
238 | if ($this->isEndOfLabels($data, $consumed)) { | |
255 | if (!isset($data[$consumed])) { | |
256 | return array(null, null); | |
257 | } | |
258 | ||
259 | $length = \ord($data[$consumed]); | |
260 | ||
261 | // end of labels reached | |
262 | if ($length === 0) { | |
239 | 263 | $consumed += 1; |
240 | 264 | break; |
241 | 265 | } |
242 | 266 | |
243 | if ($this->isCompressedLabel($data, $consumed)) { | |
244 | list($newLabels, $consumed) = $this->getCompressedLabel($data, $consumed); | |
267 | // first two bits set? this is a compressed label (14 bit pointer offset) | |
268 | if (($length & 0xc0) === 0xc0 && isset($data[$consumed + 1])) { | |
269 | $offset = ($length & ~0xc0) << 8 | \ord($data[$consumed + 1]); | |
270 | if ($offset >= $consumed) { | |
271 | return array(null, null); | |
272 | } | |
273 | ||
274 | $consumed += 2; | |
275 | list($newLabels) = $this->readLabels($data, $offset); | |
276 | ||
277 | if ($newLabels === null) { | |
278 | return array(null, null); | |
279 | } | |
280 | ||
245 | 281 | $labels = array_merge($labels, $newLabels); |
246 | 282 | break; |
247 | 283 | } |
248 | 284 | |
249 | $length = ord(substr($data, $consumed, 1)); | |
250 | $consumed += 1; | |
251 | ||
252 | if (strlen($data) - $consumed < $length) { | |
285 | // length MUST be 0-63 (6 bits only) and data has to be large enough | |
286 | if ($length & 0xc0 || !isset($data[$consumed + $length - 1])) { | |
253 | 287 | return array(null, null); |
254 | 288 | } |
255 | 289 | |
256 | $labels[] = substr($data, $consumed, $length); | |
257 | $consumed += $length; | |
290 | $labels[] = substr($data, $consumed + 1, $length); | |
291 | $consumed += $length + 1; | |
258 | 292 | } |
259 | 293 | |
260 | 294 | return array($labels, $consumed); |
261 | 295 | } |
262 | 296 | |
297 | /** | |
298 | * @deprecated unused, exists for BC only | |
299 | * @codeCoverageIgnore | |
300 | */ | |
263 | 301 | public function isEndOfLabels($data, $consumed) |
264 | 302 | { |
265 | 303 | $length = ord(substr($data, $consumed, 1)); |
266 | 304 | return 0 === $length; |
267 | 305 | } |
268 | 306 | |
307 | /** | |
308 | * @deprecated unused, exists for BC only | |
309 | * @codeCoverageIgnore | |
310 | */ | |
269 | 311 | public function getCompressedLabel($data, $consumed) |
270 | 312 | { |
271 | 313 | list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed); |
274 | 316 | return array($labels, $consumed); |
275 | 317 | } |
276 | 318 | |
319 | /** | |
320 | * @deprecated unused, exists for BC only | |
321 | * @codeCoverageIgnore | |
322 | */ | |
277 | 323 | public function isCompressedLabel($data, $consumed) |
278 | 324 | { |
279 | 325 | $mask = 0xc000; // 1100000000000000 |
282 | 328 | return (bool) ($peek & $mask); |
283 | 329 | } |
284 | 330 | |
331 | /** | |
332 | * @deprecated unused, exists for BC only | |
333 | * @codeCoverageIgnore | |
334 | */ | |
285 | 335 | public function getCompressedLabelOffset($data, $consumed) |
286 | 336 | { |
287 | 337 | $mask = 0x3fff; // 0011111111111111 |
290 | 340 | return array($peek & $mask, $consumed + 2); |
291 | 341 | } |
292 | 342 | |
343 | /** | |
344 | * @deprecated unused, exists for BC only | |
345 | * @codeCoverageIgnore | |
346 | */ | |
293 | 347 | public function signedLongToUnsignedLong($i) |
294 | 348 | { |
295 | 349 | return $i & 0x80000000 ? $i - 0xffffffff : $i; |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | use React\Dns\Model\Message; | |
5 | 4 | use React\Dns\Model\Record; |
6 | 5 | |
7 | 6 | class RecordBag |
10 | 9 | |
11 | 10 | public function set($currentTime, Record $record) |
12 | 11 | { |
13 | $this->records[$record->data] = array($currentTime + $record->ttl, $record); | |
12 | $this->records[] = array($currentTime + $record->ttl, $record); | |
14 | 13 | } |
15 | 14 | |
16 | 15 | public function all() |
1 | 1 | |
2 | 2 | namespace React\Dns\Query; |
3 | 3 | |
4 | use React\Promise\CancellablePromiseInterface; | |
4 | 5 | use React\Promise\Deferred; |
5 | 6 | |
6 | 7 | class RetryExecutor implements ExecutorInterface |
21 | 22 | |
22 | 23 | public function tryQuery($nameserver, Query $query, $retries) |
23 | 24 | { |
24 | $that = $this; | |
25 | $errorback = function ($error) use ($nameserver, $query, $retries, $that) { | |
26 | if (!$error instanceof TimeoutException) { | |
27 | throw $error; | |
25 | $deferred = new Deferred(function () use (&$promise) { | |
26 | if ($promise instanceof CancellablePromiseInterface) { | |
27 | $promise->cancel(); | |
28 | 28 | } |
29 | if (0 >= $retries) { | |
30 | throw new \RuntimeException( | |
31 | sprintf("DNS query for %s failed: too many retries", $query->name), | |
29 | }); | |
30 | ||
31 | $success = function ($value) use ($deferred, &$errorback) { | |
32 | $errorback = null; | |
33 | $deferred->resolve($value); | |
34 | }; | |
35 | ||
36 | $executor = $this->executor; | |
37 | $errorback = function ($e) use ($deferred, &$promise, $nameserver, $query, $success, &$errorback, &$retries, $executor) { | |
38 | if (!$e instanceof TimeoutException) { | |
39 | $errorback = null; | |
40 | $deferred->reject($e); | |
41 | } elseif ($retries <= 0) { | |
42 | $errorback = null; | |
43 | $deferred->reject($e = new \RuntimeException( | |
44 | 'DNS query for ' . $query->name . ' failed: too many retries', | |
32 | 45 | 0, |
33 | $error | |
46 | $e | |
47 | )); | |
48 | ||
49 | // avoid garbage references by replacing all closures in call stack. | |
50 | // what a lovely piece of code! | |
51 | $r = new \ReflectionProperty('Exception', 'trace'); | |
52 | $r->setAccessible(true); | |
53 | $trace = $r->getValue($e); | |
54 | foreach ($trace as &$one) { | |
55 | foreach ($one['args'] as &$arg) { | |
56 | if ($arg instanceof \Closure) { | |
57 | $arg = 'Object(' . \get_class($arg) . ')'; | |
58 | } | |
59 | } | |
60 | } | |
61 | $r->setValue($e, $trace); | |
62 | } else { | |
63 | --$retries; | |
64 | $promise = $executor->query($nameserver, $query)->then( | |
65 | $success, | |
66 | $errorback | |
34 | 67 | ); |
35 | 68 | } |
36 | return $that->tryQuery($nameserver, $query, $retries-1); | |
37 | 69 | }; |
38 | 70 | |
39 | return $this->executor | |
40 | ->query($nameserver, $query) | |
41 | ->then(null, $errorback); | |
71 | $promise = $this->executor->query($nameserver, $query)->then( | |
72 | $success, | |
73 | $errorback | |
74 | ); | |
75 | ||
76 | return $deferred->promise(); | |
42 | 77 | } |
43 | 78 | } |
46 | 46 | /** |
47 | 47 | * @group internet |
48 | 48 | */ |
49 | public function testResolveAllGoogleMxResolvesWithCache() | |
50 | { | |
51 | $factory = new Factory(); | |
52 | $this->resolver = $factory->createCached('8.8.8.8', $this->loop); | |
53 | ||
54 | $promise = $this->resolver->resolveAll('google.com', Message::TYPE_MX); | |
55 | $promise->then($this->expectCallableOnceWith($this->isType('array')), $this->expectCallableNever()); | |
56 | ||
57 | $this->loop->run(); | |
58 | } | |
59 | ||
60 | /** | |
61 | * @group internet | |
62 | */ | |
49 | 63 | public function testResolveInvalidRejects() |
50 | 64 | { |
51 | 65 | $ex = $this->callback(function ($param) { |
156 | 156 | $this->assertSame('178.79.169.131', $response->answers[0]->data); |
157 | 157 | } |
158 | 158 | |
159 | public function testParseAnswerWithExcessiveTtlReturnsZeroTtl() | |
160 | { | |
161 | $data = ""; | |
162 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
163 | $data .= "00 01 00 01"; // answer: type A, class IN | |
164 | $data .= "ff ff ff ff"; // answer: ttl 2^32 - 1 | |
165 | $data .= "00 04"; // answer: rdlength 4 | |
166 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 | |
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(Message::TYPE_A, $response->answers[0]->type); | |
179 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
180 | $this->assertSame(0, $response->answers[0]->ttl); | |
181 | $this->assertSame('178.79.169.131', $response->answers[0]->data); | |
182 | } | |
183 | ||
184 | public function testParseAnswerWithTtlExactlyBoundaryReturnsZeroTtl() | |
185 | { | |
186 | $data = ""; | |
187 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
188 | $data .= "00 01 00 01"; // answer: type A, class IN | |
189 | $data .= "80 00 00 00"; // answer: ttl 2^31 | |
190 | $data .= "00 04"; // answer: rdlength 4 | |
191 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 | |
192 | ||
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); | |
200 | ||
201 | $this->assertCount(1, $response->answers); | |
202 | $this->assertSame('igor.io', $response->answers[0]->name); | |
203 | $this->assertSame(Message::TYPE_A, $response->answers[0]->type); | |
204 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
205 | $this->assertSame(0, $response->answers[0]->ttl); | |
206 | $this->assertSame('178.79.169.131', $response->answers[0]->data); | |
207 | } | |
208 | ||
209 | public function testParseAnswerWithMaximumTtlReturnsExactTtl() | |
210 | { | |
211 | $data = ""; | |
212 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
213 | $data .= "00 01 00 01"; // answer: type A, class IN | |
214 | $data .= "7f ff ff ff"; // answer: ttl 2^31 - 1 | |
215 | $data .= "00 04"; // answer: rdlength 4 | |
216 | $data .= "b2 4f a9 83"; // answer: rdata 178.79.169.131 | |
217 | ||
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); | |
225 | ||
226 | $this->assertCount(1, $response->answers); | |
227 | $this->assertSame('igor.io', $response->answers[0]->name); | |
228 | $this->assertSame(Message::TYPE_A, $response->answers[0]->type); | |
229 | $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); | |
230 | $this->assertSame(0x7fffffff, $response->answers[0]->ttl); | |
231 | $this->assertSame('178.79.169.131', $response->answers[0]->data); | |
232 | } | |
233 | ||
159 | 234 | public function testParseAnswerWithUnknownType() |
160 | 235 | { |
161 | 236 | $data = ""; |
436 | 511 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io |
437 | 512 | $data .= "00 06 00 01"; // answer: type SOA, class IN |
438 | 513 | $data .= "00 01 51 80"; // answer: ttl 86400 |
439 | $data .= "00 07"; // answer: rdlength 7 | |
514 | $data .= "00 27"; // answer: rdlength 39 | |
440 | 515 | $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) |
441 | 516 | $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) |
442 | 517 | $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600 |
518 | 593 | /** |
519 | 594 | * @expectedException InvalidArgumentException |
520 | 595 | */ |
521 | public function testParseIncomplete() | |
596 | public function testParseIncompleteQuestionThrows() | |
522 | 597 | { |
523 | 598 | $data = ""; |
524 | 599 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header |
530 | 605 | $this->parser->parseMessage($data); |
531 | 606 | } |
532 | 607 | |
608 | /** | |
609 | * @expectedException InvalidArgumentException | |
610 | */ | |
611 | public function testParseIncompleteQuestionLabelThrows() | |
612 | { | |
613 | $data = ""; | |
614 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
615 | $data .= "04 69 67"; // question: ig …? | |
616 | ||
617 | $data = $this->convertTcpDumpToBinary($data); | |
618 | ||
619 | $this->parser->parseMessage($data); | |
620 | } | |
621 | ||
622 | /** | |
623 | * @expectedException InvalidArgumentException | |
624 | */ | |
625 | public function testParseIncompleteQuestionNameThrows() | |
626 | { | |
627 | $data = ""; | |
628 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
629 | $data .= "04 69 67 6f 72"; // question: igor. …? | |
630 | ||
631 | $data = $this->convertTcpDumpToBinary($data); | |
632 | ||
633 | $this->parser->parseMessage($data); | |
634 | } | |
635 | ||
636 | /** | |
637 | * @expectedException InvalidArgumentException | |
638 | */ | |
639 | public function testParseIncompleteOffsetPointerInQuestionNameThrows() | |
640 | { | |
641 | $data = ""; | |
642 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
643 | $data .= "ff"; // question: incomplete offset pointer | |
644 | ||
645 | $data = $this->convertTcpDumpToBinary($data); | |
646 | ||
647 | $this->parser->parseMessage($data); | |
648 | } | |
649 | ||
650 | /** | |
651 | * @expectedException InvalidArgumentException | |
652 | */ | |
653 | public function testParseInvalidOffsetPointerInQuestionNameThrows() | |
654 | { | |
655 | $data = ""; | |
656 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
657 | $data .= "ff ff"; // question: offset pointer to invalid address | |
658 | ||
659 | $data = $this->convertTcpDumpToBinary($data); | |
660 | ||
661 | $this->parser->parseMessage($data); | |
662 | } | |
663 | ||
664 | /** | |
665 | * @expectedException InvalidArgumentException | |
666 | */ | |
667 | public function testParseInvalidOffsetPointerToSameLabelInQuestionNameThrows() | |
668 | { | |
669 | $data = ""; | |
670 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
671 | $data .= "c0 0c"; // question: offset pointer to invalid address | |
672 | ||
673 | $data = $this->convertTcpDumpToBinary($data); | |
674 | ||
675 | $this->parser->parseMessage($data); | |
676 | } | |
677 | ||
678 | /** | |
679 | * @expectedException InvalidArgumentException | |
680 | */ | |
681 | public function testParseInvalidOffsetPointerToStartOfMessageInQuestionNameThrows() | |
682 | { | |
683 | $data = ""; | |
684 | $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header | |
685 | $data .= "c0 00"; // question: offset pointer to start of message | |
686 | ||
687 | $data = $this->convertTcpDumpToBinary($data); | |
688 | ||
689 | $this->parser->parseMessage($data); | |
690 | } | |
691 | ||
692 | /** | |
693 | * @expectedException InvalidArgumentException | |
694 | */ | |
695 | public function testParseIncompleteAnswerFieldsThrows() | |
696 | { | |
697 | $data = ""; | |
698 | $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header | |
699 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
700 | $data .= "00 01 00 01"; // question: type A, class IN | |
701 | $data .= "c0 0c"; // answer: offset pointer to igor.io | |
702 | ||
703 | $data = $this->convertTcpDumpToBinary($data); | |
704 | ||
705 | $this->parser->parseMessage($data); | |
706 | } | |
707 | ||
708 | /** | |
709 | * @expectedException InvalidArgumentException | |
710 | */ | |
711 | public function testParseIncompleteAnswerRecordDataThrows() | |
712 | { | |
713 | $data = ""; | |
714 | $data .= "72 62 81 80 00 01 00 01 00 00 00 00"; // header | |
715 | $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io | |
716 | $data .= "00 01 00 01"; // question: type A, class IN | |
717 | $data .= "c0 0c"; // answer: offset pointer to igor.io | |
718 | $data .= "00 01 00 01"; // answer: type A, class IN | |
719 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
720 | $data .= "00 04"; // answer: rdlength 4 | |
721 | ||
722 | $data = $this->convertTcpDumpToBinary($data); | |
723 | ||
724 | $this->parser->parseMessage($data); | |
725 | } | |
726 | ||
727 | public function testParseInvalidNSResponseWhereDomainNameIsMissing() | |
728 | { | |
729 | $data = ""; | |
730 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
731 | $data .= "00 02 00 01"; // answer: type NS, class IN | |
732 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
733 | $data .= "00 00"; // answer: rdlength 0 | |
734 | ||
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 | ||
746 | public function testParseInvalidAResponseWhereIPIsMissing() | |
747 | { | |
748 | $data = ""; | |
749 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
750 | $data .= "00 01 00 01"; // answer: type A, class IN | |
751 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
752 | $data .= "00 00"; // answer: rdlength 0 | |
753 | ||
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 | ||
765 | public function testParseInvalidAAAAResponseWhereIPIsMissing() | |
766 | { | |
767 | $data = ""; | |
768 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
769 | $data .= "00 1c 00 01"; // answer: type AAAA, class IN | |
770 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
771 | $data .= "00 00"; // answer: rdlength 0 | |
772 | ||
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 | ||
784 | public function testParseInvalidTXTResponseWhereTxtChunkExceedsLimit() | |
785 | { | |
786 | $data = ""; | |
787 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
788 | $data .= "00 10 00 01"; // answer: type TXT, class IN | |
789 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
790 | $data .= "00 06"; // answer: rdlength 6 | |
791 | $data .= "06 68 65 6c 6c 6f 6f"; // answer: rdata length 6: helloo | |
792 | ||
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 | ||
804 | public function testParseInvalidMXResponseWhereDomainNameIsIncomplete() | |
805 | { | |
806 | $data = ""; | |
807 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
808 | $data .= "00 0f 00 01"; // answer: type MX, class IN | |
809 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
810 | $data .= "00 08"; // answer: rdlength 8 | |
811 | $data .= "00 0a 05 68 65 6c 6c 6f"; // answer: rdata priority 10: hello (missing label end) | |
812 | ||
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 | ||
824 | public function testParseInvalidMXResponseWhereDomainNameIsMissing() | |
825 | { | |
826 | $data = ""; | |
827 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
828 | $data .= "00 0f 00 01"; // answer: type MX, class IN | |
829 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
830 | $data .= "00 02"; // answer: rdlength 2 | |
831 | $data .= "00 0a"; // answer: rdata priority 10 | |
832 | ||
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 | ||
844 | public function testParseInvalidSRVResponseWhereDomainNameIsIncomplete() | |
845 | { | |
846 | $data = ""; | |
847 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
848 | $data .= "00 21 00 01"; // answer: type SRV, class IN | |
849 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
850 | $data .= "00 0b"; // answer: rdlength 11 | |
851 | $data .= "00 0a 00 14 1F 90 04 74 65 73 74"; // answer: rdata priority 10, weight 20, port 8080 test (missing label end) | |
852 | ||
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 | ||
864 | public function testParseInvalidSRVResponseWhereDomainNameIsMissing() | |
865 | { | |
866 | $data = ""; | |
867 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
868 | $data .= "00 21 00 01"; // answer: type SRV, class IN | |
869 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
870 | $data .= "00 06"; // answer: rdlength 6 | |
871 | $data .= "00 0a 00 14 1F 90"; // answer: rdata priority 10, weight 20, port 8080 | |
872 | ||
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 | ||
884 | public function testParseInvalidSOAResponseWhereFlagsAreMissing() | |
885 | { | |
886 | $data = ""; | |
887 | $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io | |
888 | $data .= "00 06 00 01"; // answer: type SOA, class IN | |
889 | $data .= "00 01 51 80"; // answer: ttl 86400 | |
890 | $data .= "00 13"; // answer: rdlength 19 | |
891 | $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname) | |
892 | $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname) | |
893 | ||
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); | |
903 | } | |
904 | ||
533 | 905 | private function convertTcpDumpToBinary($input) |
534 | 906 | { |
535 | 907 | // sudo ngrep -d en1 -x port 53 |
38 | 38 | } |
39 | 39 | |
40 | 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 | /** | |
41 | 60 | * @covers React\Dns\Query\RecordBag |
42 | 61 | * @test |
43 | 62 | */ |
161 | 161 | $this->assertEquals(1, $cancelled); |
162 | 162 | } |
163 | 163 | |
164 | /** | |
165 | * @covers React\Dns\Query\RetryExecutor | |
166 | * @test | |
167 | */ | |
168 | public function queryShouldCancelSecondQueryOnCancel() | |
169 | { | |
170 | $deferred = new Deferred(); | |
171 | $cancelled = 0; | |
172 | ||
173 | $executor = $this->createExecutorMock(); | |
174 | $executor | |
175 | ->expects($this->exactly(2)) | |
176 | ->method('query') | |
177 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
178 | ->will($this->onConsecutiveCalls( | |
179 | $this->returnValue($deferred->promise()), | |
180 | $this->returnCallback(function ($domain, $query) use (&$cancelled) { | |
181 | $deferred = new Deferred(function ($resolve, $reject) use (&$cancelled) { | |
182 | ++$cancelled; | |
183 | $reject(new CancellationException('Cancelled')); | |
184 | }); | |
185 | ||
186 | return $deferred->promise(); | |
187 | }) | |
188 | )); | |
189 | ||
190 | $retryExecutor = new RetryExecutor($executor, 2); | |
191 | ||
192 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
193 | $promise = $retryExecutor->query('8.8.8.8', $query); | |
194 | ||
195 | $promise->then($this->expectCallableNever(), $this->expectCallableOnce()); | |
196 | ||
197 | // first query will time out after a while and this sends the next query | |
198 | $deferred->reject(new TimeoutException()); | |
199 | ||
200 | $this->assertEquals(0, $cancelled); | |
201 | $promise->cancel(); | |
202 | $this->assertEquals(1, $cancelled); | |
203 | } | |
204 | ||
205 | /** | |
206 | * @covers React\Dns\Query\RetryExecutor | |
207 | * @test | |
208 | */ | |
209 | public function queryShouldNotCauseGarbageReferencesOnSuccess() | |
210 | { | |
211 | if (class_exists('React\Promise\When')) { | |
212 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
213 | } | |
214 | ||
215 | $executor = $this->createExecutorMock(); | |
216 | $executor | |
217 | ->expects($this->once()) | |
218 | ->method('query') | |
219 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
220 | ->willReturn(Promise\resolve($this->createStandardResponse())); | |
221 | ||
222 | $retryExecutor = new RetryExecutor($executor, 0); | |
223 | ||
224 | gc_collect_cycles(); | |
225 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
226 | $retryExecutor->query('8.8.8.8', $query); | |
227 | ||
228 | $this->assertEquals(0, gc_collect_cycles()); | |
229 | } | |
230 | ||
231 | /** | |
232 | * @covers React\Dns\Query\RetryExecutor | |
233 | * @test | |
234 | */ | |
235 | public function queryShouldNotCauseGarbageReferencesOnTimeoutErrors() | |
236 | { | |
237 | if (class_exists('React\Promise\When')) { | |
238 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
239 | } | |
240 | ||
241 | $executor = $this->createExecutorMock(); | |
242 | $executor | |
243 | ->expects($this->any()) | |
244 | ->method('query') | |
245 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
246 | ->willReturn(Promise\reject(new TimeoutException("timeout"))); | |
247 | ||
248 | $retryExecutor = new RetryExecutor($executor, 0); | |
249 | ||
250 | gc_collect_cycles(); | |
251 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
252 | $retryExecutor->query('8.8.8.8', $query); | |
253 | ||
254 | $this->assertEquals(0, gc_collect_cycles()); | |
255 | } | |
256 | ||
257 | /** | |
258 | * @covers React\Dns\Query\RetryExecutor | |
259 | * @test | |
260 | */ | |
261 | public function queryShouldNotCauseGarbageReferencesOnCancellation() | |
262 | { | |
263 | if (class_exists('React\Promise\When')) { | |
264 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
265 | } | |
266 | ||
267 | $deferred = new Deferred(function () { | |
268 | throw new \RuntimeException(); | |
269 | }); | |
270 | ||
271 | $executor = $this->createExecutorMock(); | |
272 | $executor | |
273 | ->expects($this->once()) | |
274 | ->method('query') | |
275 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
276 | ->willReturn($deferred->promise()); | |
277 | ||
278 | $retryExecutor = new RetryExecutor($executor, 0); | |
279 | ||
280 | gc_collect_cycles(); | |
281 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
282 | $promise = $retryExecutor->query('8.8.8.8', $query); | |
283 | $promise->cancel(); | |
284 | $promise = null; | |
285 | ||
286 | $this->assertEquals(0, gc_collect_cycles()); | |
287 | } | |
288 | ||
289 | /** | |
290 | * @covers React\Dns\Query\RetryExecutor | |
291 | * @test | |
292 | */ | |
293 | public function queryShouldNotCauseGarbageReferencesOnNonTimeoutErrors() | |
294 | { | |
295 | if (class_exists('React\Promise\When')) { | |
296 | $this->markTestSkipped('Not supported on legacy Promise v1 API'); | |
297 | } | |
298 | ||
299 | $executor = $this->createExecutorMock(); | |
300 | $executor | |
301 | ->expects($this->once()) | |
302 | ->method('query') | |
303 | ->with('8.8.8.8', $this->isInstanceOf('React\Dns\Query\Query')) | |
304 | ->will($this->returnCallback(function ($domain, $query) { | |
305 | return Promise\reject(new \Exception); | |
306 | })); | |
307 | ||
308 | $retryExecutor = new RetryExecutor($executor, 2); | |
309 | ||
310 | gc_collect_cycles(); | |
311 | $query = new Query('igor.io', Message::TYPE_A, Message::CLASS_IN, 1345656451); | |
312 | $retryExecutor->query('8.8.8.8', $query); | |
313 | ||
314 | $this->assertEquals(0, gc_collect_cycles()); | |
315 | } | |
316 | ||
164 | 317 | protected function expectPromiseOnce($return = null) |
165 | 318 | { |
166 | 319 | $mock = $this->createPromiseMock(); |