diff --git a/examples/04-query-a-and-aaaa.php b/examples/10-query-a-and-aaaa.php similarity index 100% rename from examples/04-query-a-and-aaaa.php rename to examples/10-query-a-and-aaaa.php diff --git a/examples/11-query-any.php b/examples/11-query-any.php new file mode 100644 index 00000000..c7871bdb --- /dev/null +++ b/examples/11-query-any.php @@ -0,0 +1,50 @@ +query('8.8.8.8:53', $any)->then(function (Message $message) { + foreach ($message->answers as $answer) { + /* @var $answer Record */ + + $data = $answer->data; + + switch ($answer->type) { + case Message::TYPE_A: + $type = 'A'; + break; + case Message::TYPE_AAAA: + $type = 'AAAA'; + break; + case Message::TYPE_NS: + $type = 'NS'; + break; + case Message::TYPE_PTR: + $type = 'PTR'; + break; + case Message::TYPE_CNAME: + $type = 'CNAME'; + break; + default: + // unknown type uses HEX format + $type = 'Type ' . $answer->type; + $data = wordwrap(strtoupper(bin2hex($data)), 2, ' ', true); + } + + echo $type . ': ' . $data . PHP_EOL; + } +}, 'printf'); + +$loop->run(); diff --git a/src/Model/Message.php b/src/Model/Message.php index 1b7421c6..d318cdb0 100644 --- a/src/Model/Message.php +++ b/src/Model/Message.php @@ -14,6 +14,7 @@ class Message const TYPE_MX = 15; const TYPE_TXT = 16; const TYPE_AAAA = 28; + const TYPE_ANY = 255; const CLASS_IN = 1; diff --git a/src/Model/Record.php b/src/Model/Record.php index 029d2324..387b2b27 100644 --- a/src/Model/Record.php +++ b/src/Model/Record.php @@ -4,10 +4,49 @@ class Record { + /** + * @var string hostname without trailing dot, for example "reactphp.org" + */ public $name; + + /** + * @var int see Message::TYPE_* constants (UINT16) + */ public $type; + + /** + * @var int see Message::CLASS_IN constant (UINT16) + */ public $class; + + /** + * @var int maximum TTL in seconds (UINT16) + */ public $ttl; + + /** + * The payload data for this record + * + * The payload data format depends on the record type. As a rule of thumb, + * this library will try to express this in a way that can be consumed + * easily without having to worry about DNS internals and its binary transport: + * + * - A: + * IPv4 address string, for example "192.168.1.1". + * - AAAA: + * IPv6 address string, for example "::1". + * - CNAME / PTR / NS: + * The hostname without trailing dot, for example "reactphp.org". + * - Any other unknown type: + * An opaque binary string containing the RDATA as transported in the DNS + * record. For forwards compatibility, you should not rely on this format + * for unknown types. Future versions may add support for new types and + * this may then parse the payload data appropriately - this will not be + * considered a BC break. See the format definition of known types above + * for more details. + * + * @var string + */ public $data; public function __construct($name, $type, $class, $ttl = 0, $data = null) diff --git a/src/Protocol/Parser.php b/src/Protocol/Parser.php index 1191cd31..b2c03bd7 100644 --- a/src/Protocol/Parser.php +++ b/src/Protocol/Parser.php @@ -164,12 +164,14 @@ public function parseAnswer(Message $message) $consumed += $rdLength; $rdata = inet_ntop($ip); - } - - if (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type) { + } elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) { list($bodyLabels, $consumed) = $this->readLabels($message->data, $consumed); $rdata = implode('.', $bodyLabels); + } else { + // unknown types simply parse rdata as an opaque binary string + $rdata = substr($message->data, $consumed, $rdLength); + $consumed += $rdLength; } $message->consumed = $consumed; diff --git a/tests/Protocol/ParserTest.php b/tests/Protocol/ParserTest.php index 195fad24..e5d908e7 100644 --- a/tests/Protocol/ParserTest.php +++ b/tests/Protocol/ParserTest.php @@ -157,6 +157,31 @@ public function testParseAnswerWithInlineData() $this->assertSame('178.79.169.131', $response->answers[0]->data); } + public function testParseAnswerWithUnknownType() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "23 28 00 01"; // answer: type 9000, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 05"; // answer: rdlength 5 + $data .= "68 65 6c 6c 6f"; // answer: rdata "hello" + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(1, $response->answers); + $this->assertSame('igor.io', $response->answers[0]->name); + $this->assertSame(9000, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(86400, $response->answers[0]->ttl); + $this->assertSame('hello', $response->answers[0]->data); + } + public function testParseResponseWithCnameAndOffsetPointers() { $data = ""; @@ -273,6 +298,31 @@ public function testParseResponseWithTwoAnswers() $this->assertSame('193.223.78.152', $response->answers[1]->data); } + public function testParseNSResponse() + { + $data = ""; + $data .= "04 69 67 6f 72 02 69 6f 00"; // answer: igor.io + $data .= "00 02 00 01"; // answer: type NS, class IN + $data .= "00 01 51 80"; // answer: ttl 86400 + $data .= "00 07"; // answer: rdlength 7 + $data .= "05 68 65 6c 6c 6f 00"; // answer: rdata hello + + $data = $this->convertTcpDumpToBinary($data); + + $response = new Message(); + $response->header->set('anCount', 1); + $response->data = $data; + + $this->parser->parseAnswer($response); + + $this->assertCount(1, $response->answers); + $this->assertSame('igor.io', $response->answers[0]->name); + $this->assertSame(Message::TYPE_NS, $response->answers[0]->type); + $this->assertSame(Message::CLASS_IN, $response->answers[0]->class); + $this->assertSame(86400, $response->answers[0]->ttl); + $this->assertSame('hello', $response->answers[0]->data); + } + public function testParsePTRResponse() { $data = "";