Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions src/Model/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

/**
Expand Down
7 changes: 7 additions & 0 deletions src/Model/Record.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
73 changes: 58 additions & 15 deletions src/Protocol/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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)));
Expand All @@ -148,7 +199,7 @@ public function parseAnswer(Message $message)
$consumed += 2;

if (!isset($message->data[$consumed + $rdLength - 1])) {
return;
return null;
}

$rdata = null;
Expand Down Expand Up @@ -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)
Expand Down
123 changes: 122 additions & 1 deletion tests/Protocol/ParserTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = "";
Expand Down Expand Up @@ -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
*/
Expand Down