Codebase list reactphp-dns / upstream/0.4.16
New upstream version 0.4.16 Dominik George 5 years ago
10 changed file(s) with 756 addition(s) and 94 deletion(s). Raw diff Collapse all Expand all
00 # 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)
116
217 ## 0.4.15 (2018-07-02)
318
1328 * Feature: Support parsing `NS`, `TXT`, `MX`, `SOA` and `SRV` records.
1429 (#104, #105, #106, #107 and #108 by @clue)
1530
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.
1732 (#104 by @clue)
1833
1934 * Feature: Improve error messages for failed queries and improve documentation.
280280 This will install the latest supported version:
281281
282282 ```bash
283 $ composer require react/dns:^0.4.15
283 $ composer require react/dns:^0.4.16
284284 ```
285285
286286 See also the [CHANGELOG](CHANGELOG.md) for details about version upgrades.
1919 public $class;
2020
2121 /**
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
2324 */
2425 public $ttl;
2526
3131
3232 /**
3333 * @deprecated unused, exists for BC only
34 * @codeCoverageIgnore
3435 */
3536 public function parseChunk($data, Message $message)
3637 {
6465
6566 public function parseHeader(Message $message)
6667 {
67 if (strlen($message->data) < 12) {
68 if (!isset($message->data[12 - 1])) {
6869 return;
6970 }
7071
9596
9697 public function parseQuestion(Message $message)
9798 {
98 if (strlen($message->data) < 2) {
99 return;
100 }
101
10299 $consumed = $message->consumed;
103100
104101 list($labels, $consumed) = $this->readLabels($message->data, $consumed);
105102
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])) {
111104 return;
112105 }
113106
131124
132125 public function parseAnswer(Message $message)
133126 {
134 if (strlen($message->data) < 2) {
135 return;
136 }
137
138127 $consumed = $message->consumed;
139128
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])) {
147132 return;
148133 }
149134
153138 list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4)));
154139 $consumed += 4;
155140
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
156146 list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2)));
157147 $consumed += 2;
158148
149 if (!isset($message->data[$consumed + $rdLength - 1])) {
150 return;
151 }
152
159153 $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 }
166166 } 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);
170168 } elseif (Message::TYPE_TXT === $type) {
171169 $rdata = array();
172 $remaining = $rdLength;
173 while ($remaining) {
170 while ($consumed < $expected) {
174171 $len = ord($message->data[$consumed]);
175 $rdata[] = substr($message->data, $consumed + 1, $len);
172 $rdata[] = (string)substr($message->data, $consumed + 1, $len);
176173 $consumed += $len + 1;
177 $remaining -= $len + 1;
178174 }
179175 } 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 }
187185 } 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 }
197197 } 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 }
212215 } else {
213216 // unknown types simply parse rdata as an opaque binary string
214217 $rdata = substr($message->data, $consumed, $rdLength);
215218 $consumed += $rdLength;
216219 }
217220
221 // ensure parsing record data consumes expact number of bytes indicated in record length
222 if ($consumed !== $expected || $rdata === null) {
223 return;
224 }
225
218226 $message->consumed = $consumed;
219227
220 $name = implode('.', $labels);
221 $ttl = $this->signedLongToUnsignedLong($ttl);
222228 $record = new Record($name, $type, $class, $ttl, $rdata);
223229
224230 $message->answers[] = $record;
230236 return $message;
231237 }
232238
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
233250 private function readLabels($data, $consumed)
234251 {
235252 $labels = array();
236253
237254 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) {
239263 $consumed += 1;
240264 break;
241265 }
242266
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
245281 $labels = array_merge($labels, $newLabels);
246282 break;
247283 }
248284
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])) {
253287 return array(null, null);
254288 }
255289
256 $labels[] = substr($data, $consumed, $length);
257 $consumed += $length;
290 $labels[] = substr($data, $consumed + 1, $length);
291 $consumed += $length + 1;
258292 }
259293
260294 return array($labels, $consumed);
261295 }
262296
297 /**
298 * @deprecated unused, exists for BC only
299 * @codeCoverageIgnore
300 */
263301 public function isEndOfLabels($data, $consumed)
264302 {
265303 $length = ord(substr($data, $consumed, 1));
266304 return 0 === $length;
267305 }
268306
307 /**
308 * @deprecated unused, exists for BC only
309 * @codeCoverageIgnore
310 */
269311 public function getCompressedLabel($data, $consumed)
270312 {
271313 list($nameOffset, $consumed) = $this->getCompressedLabelOffset($data, $consumed);
274316 return array($labels, $consumed);
275317 }
276318
319 /**
320 * @deprecated unused, exists for BC only
321 * @codeCoverageIgnore
322 */
277323 public function isCompressedLabel($data, $consumed)
278324 {
279325 $mask = 0xc000; // 1100000000000000
282328 return (bool) ($peek & $mask);
283329 }
284330
331 /**
332 * @deprecated unused, exists for BC only
333 * @codeCoverageIgnore
334 */
285335 public function getCompressedLabelOffset($data, $consumed)
286336 {
287337 $mask = 0x3fff; // 0011111111111111
290340 return array($peek & $mask, $consumed + 2);
291341 }
292342
343 /**
344 * @deprecated unused, exists for BC only
345 * @codeCoverageIgnore
346 */
293347 public function signedLongToUnsignedLong($i)
294348 {
295349 return $i & 0x80000000 ? $i - 0xffffffff : $i;
11
22 namespace React\Dns\Query;
33
4 use React\Dns\Model\Message;
54 use React\Dns\Model\Record;
65
76 class RecordBag
109
1110 public function set($currentTime, Record $record)
1211 {
13 $this->records[$record->data] = array($currentTime + $record->ttl, $record);
12 $this->records[] = array($currentTime + $record->ttl, $record);
1413 }
1514
1615 public function all()
11
22 namespace React\Dns\Query;
33
4 use React\Promise\CancellablePromiseInterface;
45 use React\Promise\Deferred;
56
67 class RetryExecutor implements ExecutorInterface
2122
2223 public function tryQuery($nameserver, Query $query, $retries)
2324 {
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();
2828 }
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',
3245 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
3467 );
3568 }
36 return $that->tryQuery($nameserver, $query, $retries-1);
3769 };
3870
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();
4277 }
4378 }
4646 /**
4747 * @group internet
4848 */
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 */
4963 public function testResolveInvalidRejects()
5064 {
5165 $ex = $this->callback(function ($param) {
156156 $this->assertSame('178.79.169.131', $response->answers[0]->data);
157157 }
158158
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
159234 public function testParseAnswerWithUnknownType()
160235 {
161236 $data = "";
436511 $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io
437512 $data .= "00 06 00 01"; // answer: type SOA, class IN
438513 $data .= "00 01 51 80"; // answer: ttl 86400
439 $data .= "00 07"; // answer: rdlength 7
514 $data .= "00 27"; // answer: rdlength 39
440515 $data .= "02 6e 73 05 68 65 6c 6c 6f 00"; // answer: rdata ns.hello (mname)
441516 $data .= "01 65 05 68 65 6c 6c 6f 00"; // answer: rdata e.hello (rname)
442517 $data .= "78 49 28 D5 00 00 2a 30 00 00 0e 10"; // answer: rdata 2018060501, 10800, 3600
518593 /**
519594 * @expectedException InvalidArgumentException
520595 */
521 public function testParseIncomplete()
596 public function testParseIncompleteQuestionThrows()
522597 {
523598 $data = "";
524599 $data .= "72 62 01 00 00 01 00 00 00 00 00 00"; // header
530605 $this->parser->parseMessage($data);
531606 }
532607
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
533905 private function convertTcpDumpToBinary($input)
534906 {
535907 // sudo ngrep -d en1 -x port 53
3838 }
3939
4040 /**
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 /**
4160 * @covers React\Dns\Query\RecordBag
4261 * @test
4362 */
161161 $this->assertEquals(1, $cancelled);
162162 }
163163
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
164317 protected function expectPromiseOnce($return = null)
165318 {
166319 $mock = $this->createPromiseMock();