diff --git a/src/Model/Message.php b/src/Model/Message.php index 167344db..da859e7e 100644 --- a/src/Model/Message.php +++ b/src/Model/Message.php @@ -100,10 +100,44 @@ private static function generateId() return mt_rand(0, 0xffff); } + /** + * @var HeaderBag + */ public $header; + + /** + * This should be an array of Query objects. For BC reasons, this currently + * references a nested array with a structure that results from casting the + * Query objects to an array: + * + * ```php + * $questions = array( + * array( + * 'name' => 'reactphp.org', + * 'type' => Message::TYPE_A, + * 'class' => Message::CLASS_IN + * ) + * ); + * ``` + * + * @var array + * @see Query + */ public $questions = array(); + + /** + * @var Record[] + */ public $answers = array(); + + /** + * @var Record[] + */ public $authority = array(); + + /** + * @var Record[] + */ public $additional = array(); /** diff --git a/src/Model/Record.php b/src/Model/Record.php index 7507fcbf..2504911b 100644 --- a/src/Model/Record.php +++ b/src/Model/Record.php @@ -87,6 +87,13 @@ class Record */ public $data; + /** + * @param string $name + * @param int $type + * @param int $class + * @param int $ttl + * @param string|string[]|array $data + */ public function __construct($name, $type, $class, $ttl = 0, $data = null) { $this->name = $name; diff --git a/src/Protocol/Parser.php b/src/Protocol/Parser.php index 56cc9490..ada9db14 100644 --- a/src/Protocol/Parser.php +++ b/src/Protocol/Parser.php @@ -55,9 +55,33 @@ private function parse($data, Message $message) } } - if ($message->header->get('anCount') != count($message->answers)) { - if (!$this->parseAnswer($message)) { + // parse all answer records + for ($i = $message->header->get('anCount'); $i > 0; --$i) { + $record = $this->parseRecord($message); + if ($record === null) { + return; + } else { + $message->answers[] = $record; + } + } + + // parse all authority records + for ($i = $message->header->get('nsCount'); $i > 0; --$i) { + $record = $this->parseRecord($message); + if ($record === null) { return; + } else { + $message->authority[] = $record; + } + } + + // parse all additional records + for ($i = $message->header->get('arCount'); $i > 0; --$i) { + $record = $this->parseRecord($message); + if ($record === null) { + return; + } else { + $message->additional[] = $record; } } @@ -87,7 +111,6 @@ public function parseHeader(Message $message) $vars = compact('id', 'qdCount', 'anCount', 'nsCount', 'arCount', 'qr', 'opcode', 'aa', 'tc', 'rd', 'ra', 'z', 'rcode'); - foreach ($vars as $name => $value) { $message->header->set($name, $value); } @@ -123,14 +146,42 @@ public function parseQuestion(Message $message) return $message; } + /** + * recursively parse all answers from the message data into message answer records + * + * @param Message $message + * @return ?Message returns the updated message on success or null if the data is invalid/incomplete + * @deprecated unused, exists for BC only + * @codeCoverageIgnore + */ public function parseAnswer(Message $message) + { + $record = $this->parseRecord($message); + if ($record === null) { + return null; + } + + $message->answers[] = $record; + + if ($message->header->get('anCount') != count($message->answers)) { + return $this->parseAnswer($message); + } + + return $message; + } + + /** + * @param Message $message + * @return ?Record returns parsed Record on success or null if data is invalid/incomplete + */ + private function parseRecord(Message $message) { $consumed = $message->consumed; list($name, $consumed) = $this->readDomain($message->data, $consumed); if ($name === null || !isset($message->data[$consumed + 10 - 1])) { - return; + return null; } list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4))); @@ -148,7 +199,7 @@ public function parseAnswer(Message $message) $consumed += 2; if (!isset($message->data[$consumed + $rdLength - 1])) { - return; + return null; } $rdata = null; @@ -221,20 +272,12 @@ public function parseAnswer(Message $message) // ensure parsing record data consumes expact number of bytes indicated in record length if ($consumed !== $expected || $rdata === null) { - return; + return null; } $message->consumed = $consumed; - $record = new Record($name, $type, $class, $ttl, $rdata); - - $message->answers[] = $record; - - if ($message->header->get('anCount') != count($message->answers)) { - return $this->parseAnswer($message); - } - - return $message; + return new Record($name, $type, $class, $ttl, $rdata); } private function readDomain($data, $consumed) diff --git a/tests/Protocol/ParserTest.php b/tests/Protocol/ParserTest.php index aed4e454..40866262 100644 --- a/tests/Protocol/ParserTest.php +++ b/tests/Protocol/ParserTest.php @@ -439,7 +439,7 @@ public function testParseSRVResponse() ); } - public function testParseResponseWithTwoAnswers() + public function testParseMessageResponseWithTwoAnswers() { $data = ""; $data .= "bc 73 81 80 00 01 00 02 00 00 00 00"; // header @@ -481,6 +481,95 @@ public function testParseResponseWithTwoAnswers() $this->assertSame('193.223.78.152', $response->answers[1]->data); } + public function testParseMessageResponseWithTwoAuthorityRecords() + { + $data = ""; + $data .= "bc 73 81 80 00 01 00 00 00 02 00 00"; // header + $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; + // question: io.whois-servers.net + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // authority: offset pointer to io.whois-servers.net + $data .= "00 05 00 01"; // authority: type CNAME, class IN + $data .= "00 00 00 29"; // authority: ttl 41 + $data .= "00 0e"; // authority: rdlength 14 + $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // authority: rdata whois.nic.io + $data .= "c0 32"; // authority: offset pointer to whois.nic.io + $data .= "00 01 00 01"; // authority: type CNAME, class IN + $data .= "00 00 0d f7"; // authority: ttl 3575 + $data .= "00 04"; // authority: rdlength 4 + $data .= "c1 df 4e 98"; // authority: rdata 193.223.78.152 + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $this->assertCount(1, $response->questions); + $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(0, $response->answers); + + $this->assertCount(2, $response->authority); + + $this->assertSame('io.whois-servers.net', $response->authority[0]->name); + $this->assertSame(Message::TYPE_CNAME, $response->authority[0]->type); + $this->assertSame(Message::CLASS_IN, $response->authority[0]->class); + $this->assertSame(41, $response->authority[0]->ttl); + $this->assertSame('whois.nic.io', $response->authority[0]->data); + + $this->assertSame('whois.nic.io', $response->authority[1]->name); + $this->assertSame(Message::TYPE_A, $response->authority[1]->type); + $this->assertSame(Message::CLASS_IN, $response->authority[1]->class); + $this->assertSame(3575, $response->authority[1]->ttl); + $this->assertSame('193.223.78.152', $response->authority[1]->data); + } + + public function testParseMessageResponseWithAnswerAndAdditionalRecord() + { + $data = ""; + $data .= "bc 73 81 80 00 01 00 01 00 00 00 01"; // header + $data .= "02 69 6f 0d 77 68 6f 69 73 2d 73 65 72 76 65 72 73 03 6e 65 74 00"; + // question: io.whois-servers.net + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // answer: offset pointer to io.whois-servers.net + $data .= "00 05 00 01"; // answer: type CNAME, class IN + $data .= "00 00 00 29"; // answer: ttl 41 + $data .= "00 0e"; // answer: rdlength 14 + $data .= "05 77 68 6f 69 73 03 6e 69 63 02 69 6f 00"; // answer: rdata whois.nic.io + $data .= "c0 32"; // additional: offset pointer to whois.nic.io + $data .= "00 01 00 01"; // additional: type CNAME, class IN + $data .= "00 00 0d f7"; // additional: ttl 3575 + $data .= "00 04"; // additional: rdlength 4 + $data .= "c1 df 4e 98"; // additional: rdata 193.223.78.152 + + $data = $this->convertTcpDumpToBinary($data); + + $response = $this->parser->parseMessage($data); + + $this->assertCount(1, $response->questions); + $this->assertSame('io.whois-servers.net', $response->questions[0]['name']); + $this->assertSame(Message::TYPE_A, $response->questions[0]['type']); + $this->assertSame(Message::CLASS_IN, $response->questions[0]['class']); + + $this->assertCount(1, $response->answers); + + $this->assertSame('io.whois-servers.net', $response->answers[0]->name); + $this->assertSame(Message::TYPE_CNAME, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(41, $response->answers[0]->ttl); + $this->assertSame('whois.nic.io', $response->answers[0]->data); + + $this->assertCount(0, $response->authority); + $this->assertCount(1, $response->additional); + + $this->assertSame('whois.nic.io', $response->additional[0]->name); + $this->assertSame(Message::TYPE_A, $response->additional[0]->type); + $this->assertSame(Message::CLASS_IN, $response->additional[0]->class); + $this->assertSame(3575, $response->additional[0]->ttl); + $this->assertSame('193.223.78.152', $response->additional[0]->data); + } + public function testParseNSResponse() { $data = ""; @@ -706,6 +795,38 @@ public function testParseIncompleteAnswerFieldsThrows() $this->parser->parseMessage($data); } + /** + * @expectedException InvalidArgumentException + */ + public function testParseMessageResponseWithIncompleteAuthorityRecordThrows() + { + $data = ""; + $data .= "72 62 81 80 00 01 00 00 00 01 00 00"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // authority: offset pointer to igor.io + + $data = $this->convertTcpDumpToBinary($data); + + $this->parser->parseMessage($data); + } + + /** + * @expectedException InvalidArgumentException + */ + public function testParseMessageResponseWithIncompleteAdditionalRecordThrows() + { + $data = ""; + $data .= "72 62 81 80 00 01 00 00 00 00 00 01"; // header + $data .= "04 69 67 6f 72 02 69 6f 00"; // question: igor.io + $data .= "00 01 00 01"; // question: type A, class IN + $data .= "c0 0c"; // additional: offset pointer to igor.io + + $data = $this->convertTcpDumpToBinary($data); + + $this->parser->parseMessage($data); + } + /** * @expectedException InvalidArgumentException */