Skip to content
Merged
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
150 changes: 75 additions & 75 deletions src/Protocol/Parser.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,29 +23,28 @@ final class Parser
*/
public function parseMessage($data)
{
// create empty message with two additional, temporary properties for parser
$message = new Message();
$message->data = $data;
$message->consumed = null;

if ($this->parse($data, $message) !== $message) {
$message = $this->parse($data, 0);
if ($message === null) {
throw new InvalidArgumentException('Unable to parse binary message');
}

unset($message->data, $message->consumed);

return $message;
}

private function parse($data, Message $message)
/**
* @param string $data
* @param int $consumed
* @return ?Message
*/
private function parse($data, $consumed)
{
if (!isset($message->data[12 - 1])) {
return;
if (!isset($data[12 - 1])) {
return null;
}

list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($message->data, 0, 12)));
$message->consumed += 12;
list($id, $fields, $qdCount, $anCount, $nsCount, $arCount) = array_values(unpack('n*', substr($data, 0, 12)));

$message = new Message();
$message->id = $id;
$message->rcode = $fields & 0xf;
$message->ra = (($fields >> 7) & 1) === 1;
Expand All @@ -54,42 +53,43 @@ private function parse($data, Message $message)
$message->aa = (($fields >> 10) & 1) === 1;
$message->opcode = ($fields >> 11) & 0xf;
$message->qr = (($fields >> 15) & 1) === 1;
$consumed += 12;

// parse all questions
for ($i = $qdCount; $i > 0; --$i) {
$question = $this->parseQuestion($message);
list($question, $consumed) = $this->parseQuestion($data, $consumed);
if ($question === null) {
return;
return null;
} else {
$message->questions[] = $question;
}
}

// parse all answer records
for ($i = $anCount; $i > 0; --$i) {
$record = $this->parseRecord($message);
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return;
return null;
} else {
$message->answers[] = $record;
}
}

// parse all authority records
for ($i = $nsCount; $i > 0; --$i) {
$record = $this->parseRecord($message);
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return;
return null;
} else {
$message->authority[] = $record;
}
}

// parse all additional records
for ($i = $arCount; $i > 0; --$i) {
$record = $this->parseRecord($message);
list($record, $consumed) = $this->parseRecord($data, $consumed);
if ($record === null) {
return;
return null;
} else {
$message->additional[] = $record;
}
Expand All @@ -99,89 +99,88 @@ private function parse($data, Message $message)
}

/**
* @param Message $message
* @return ?Query
* @param string $data
* @param int $consumed
* @return array
*/
private function parseQuestion(Message $message)
private function parseQuestion($data, $consumed)
{
$consumed = $message->consumed;

list($labels, $consumed) = $this->readLabels($message->data, $consumed);
list($labels, $consumed) = $this->readLabels($data, $consumed);

if ($labels === null || !isset($message->data[$consumed + 4 - 1])) {
return;
if ($labels === null || !isset($data[$consumed + 4 - 1])) {
return array(null, null);
}

list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
$consumed += 4;

$message->consumed = $consumed;

return new Query(
implode('.', $labels),
$type,
$class
return array(
new Query(
implode('.', $labels),
$type,
$class
),
$consumed
);
}

/**
* @param Message $message
* @return ?Record returns parsed Record on success or null if data is invalid/incomplete
* @param string $data
* @param int $consumed
* @return array An array with a parsed Record on success or array with null if data is invalid/incomplete
*/
private function parseRecord(Message $message)
private function parseRecord($data, $consumed)
{
$consumed = $message->consumed;

list($name, $consumed) = $this->readDomain($message->data, $consumed);
list($name, $consumed) = $this->readDomain($data, $consumed);

if ($name === null || !isset($message->data[$consumed + 10 - 1])) {
return null;
if ($name === null || !isset($data[$consumed + 10 - 1])) {
return array(null, null);
}

list($type, $class) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
list($type, $class) = array_values(unpack('n*', substr($data, $consumed, 4)));
$consumed += 4;

list($ttl) = array_values(unpack('N', substr($message->data, $consumed, 4)));
list($ttl) = array_values(unpack('N', substr($data, $consumed, 4)));
$consumed += 4;

// TTL is a UINT32 that must not have most significant bit set for BC reasons
if ($ttl < 0 || $ttl >= 1 << 31) {
$ttl = 0;
}

list($rdLength) = array_values(unpack('n', substr($message->data, $consumed, 2)));
list($rdLength) = array_values(unpack('n', substr($data, $consumed, 2)));
$consumed += 2;

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

$rdata = null;
$expected = $consumed + $rdLength;

if (Message::TYPE_A === $type) {
if ($rdLength === 4) {
$rdata = inet_ntop(substr($message->data, $consumed, $rdLength));
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
$consumed += $rdLength;
}
} elseif (Message::TYPE_AAAA === $type) {
if ($rdLength === 16) {
$rdata = inet_ntop(substr($message->data, $consumed, $rdLength));
$rdata = inet_ntop(substr($data, $consumed, $rdLength));
$consumed += $rdLength;
}
} elseif (Message::TYPE_CNAME === $type || Message::TYPE_PTR === $type || Message::TYPE_NS === $type) {
list($rdata, $consumed) = $this->readDomain($message->data, $consumed);
list($rdata, $consumed) = $this->readDomain($data, $consumed);
} elseif (Message::TYPE_TXT === $type || Message::TYPE_SPF === $type) {
$rdata = array();
while ($consumed < $expected) {
$len = ord($message->data[$consumed]);
$rdata[] = (string)substr($message->data, $consumed + 1, $len);
$len = ord($data[$consumed]);
$rdata[] = (string)substr($data, $consumed + 1, $len);
$consumed += $len + 1;
}
} elseif (Message::TYPE_MX === $type) {
if ($rdLength > 2) {
list($priority) = array_values(unpack('n', substr($message->data, $consumed, 2)));
list($target, $consumed) = $this->readDomain($message->data, $consumed + 2);
list($priority) = array_values(unpack('n', substr($data, $consumed, 2)));
list($target, $consumed) = $this->readDomain($data, $consumed + 2);

$rdata = array(
'priority' => $priority,
Expand All @@ -190,8 +189,8 @@ private function parseRecord(Message $message)
}
} elseif (Message::TYPE_SRV === $type) {
if ($rdLength > 6) {
list($priority, $weight, $port) = array_values(unpack('n*', substr($message->data, $consumed, 6)));
list($target, $consumed) = $this->readDomain($message->data, $consumed + 6);
list($priority, $weight, $port) = array_values(unpack('n*', substr($data, $consumed, 6)));
list($target, $consumed) = $this->readDomain($data, $consumed + 6);

$rdata = array(
'priority' => $priority,
Expand All @@ -202,8 +201,8 @@ private function parseRecord(Message $message)
}
} elseif (Message::TYPE_SSHFP === $type) {
if ($rdLength > 2) {
list($algorithm, $hash) = \array_values(\unpack('C*', \substr($message->data, $consumed, 2)));
$fingerprint = \bin2hex(\substr($message->data, $consumed + 2, $rdLength - 2));
list($algorithm, $hash) = \array_values(\unpack('C*', \substr($data, $consumed, 2)));
$fingerprint = \bin2hex(\substr($data, $consumed + 2, $rdLength - 2));
$consumed += $rdLength;

$rdata = array(
Expand All @@ -213,11 +212,11 @@ private function parseRecord(Message $message)
);
}
} elseif (Message::TYPE_SOA === $type) {
list($mname, $consumed) = $this->readDomain($message->data, $consumed);
list($rname, $consumed) = $this->readDomain($message->data, $consumed);
list($mname, $consumed) = $this->readDomain($data, $consumed);
list($rname, $consumed) = $this->readDomain($data, $consumed);

if ($mname !== null && $rname !== null && isset($message->data[$consumed + 20 - 1])) {
list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($message->data, $consumed, 20)));
if ($mname !== null && $rname !== null && isset($data[$consumed + 20 - 1])) {
list($serial, $refresh, $retry, $expire, $minimum) = array_values(unpack('N*', substr($data, $consumed, 20)));
$consumed += 20;

$rdata = array(
Expand All @@ -232,9 +231,9 @@ private function parseRecord(Message $message)
}
} elseif (Message::TYPE_OPT === $type) {
$rdata = array();
while (isset($message->data[$consumed + 4 - 1])) {
list($code, $length) = array_values(unpack('n*', substr($message->data, $consumed, 4)));
$value = (string) substr($message->data, $consumed + 4, $length);
while (isset($data[$consumed + 4 - 1])) {
list($code, $length) = array_values(unpack('n*', substr($data, $consumed, 4)));
$value = (string) substr($data, $consumed + 4, $length);
if ($code === Message::OPT_TCP_KEEPALIVE && $value === '') {
$value = null;
} elseif ($code === Message::OPT_TCP_KEEPALIVE && $length === 2) {
Expand All @@ -248,11 +247,11 @@ private function parseRecord(Message $message)
}
} elseif (Message::TYPE_CAA === $type) {
if ($rdLength > 3) {
list($flag, $tagLength) = array_values(unpack('C*', substr($message->data, $consumed, 2)));
list($flag, $tagLength) = array_values(unpack('C*', substr($data, $consumed, 2)));

if ($tagLength > 0 && $rdLength - 2 - $tagLength > 0) {
$tag = substr($message->data, $consumed + 2, $tagLength);
$value = substr($message->data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
$tag = substr($data, $consumed + 2, $tagLength);
$value = substr($data, $consumed + 2 + $tagLength, $rdLength - 2 - $tagLength);
$consumed += $rdLength;

$rdata = array(
Expand All @@ -264,18 +263,19 @@ private function parseRecord(Message $message)
}
} else {
// unknown types simply parse rdata as an opaque binary string
$rdata = substr($message->data, $consumed, $rdLength);
$rdata = substr($data, $consumed, $rdLength);
$consumed += $rdLength;
}

// ensure parsing record data consumes expact number of bytes indicated in record length
if ($consumed !== $expected || $rdata === null) {
return null;
return array(null, null);
}

$message->consumed = $consumed;

return new Record($name, $type, $class, $ttl, $rdata);
return array(
new Record($name, $type, $class, $ttl, $rdata),
$consumed
);
}

private function readDomain($data, $consumed)
Expand Down