dns/Query.php
<?php
namespace Async\Dns;
/**
* This file is the PurplePixie PHP DNS Query Class
*
* The software is (C) Copyright 2008-16 PurplePixie Systems
*
* This is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* The software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this software. If not, see www.gnu.org/licenses
*
* For more information see www.purplepixie.org/phpdns
*/
class Query
{
/**
* @var string
*/
private $server = '';
/**
* @var int
*/
private $port = 0;
/**
* @var int
*/
private $timeout = 0; // default set in constructor
/**
* @var bool
*/
private $udp = false;
/**
* @var bool
*/
private $debug = false;
/**
* @var bool
*/
private $binaryDebug = false;
/**
* @var Types
*/
private $types;
/**
* @var string
*/
private $rawBuffer = '';
/**
* @var string
*/
private $rawHeader = '';
/**
* @var string
*/
private $rawResponse = '';
/**
* @var array
*/
private $header = [];
/**
* @var int
*/
private $responseCounter = 0;
/**
* @var Answer
*/
private $lastNameservers;
/**
* @var Answer
*/
private $lastAdditional;
/**
* @var bool
*/
private $error = false;
/**
* @var string
*/
private $lastError = '';
/**
* @param string $server
* @param int $port
* @param int $timeout
* @param bool $udp
* @param bool $debug
* @param bool $binaryDebug
*/
public function __construct($server = null, $port = 53, $timeout = 60, $udp = true, $debug = false, $binaryDebug = false)
{
if (empty($server))
throw new \InvalidArgumentException('Missing server argument');
$this->server = $server;
$this->port = $port;
$this->timeout = $timeout;
$this->udp = $udp;
$this->debug = $debug;
$this->binaryDebug = $binaryDebug;
$this->types = new Types();
$this->debug('Query Class Initialized');
}
/**
* @param string $question
* @param string $type
*
* @return Answer
* @throws \Exception
*/
public function query($question, $type = 'A'): Answer
{
$this->clearError();
$typeId = $this->types->getByName($type);
if ($typeId === false) {
$this->setError('Invalid Query Type ' . $type);
throw new \Exception('Invalid Query Type ' . $type);
}
if ($this->udp) {
$host = 'udp://' . $this->server;
} else {
$host = $this->server;
}
$errno = 0;
$errstr = '';
if (!$socket = @\fsockopen($host, $this->port, $errno, $errstr, $this->timeout)) {
$this->setError('Failed to Open Socket');
throw new \Exception('Failed to Open Socket');
}
// handles timeout on stream read set using timeout as well
\stream_set_timeout($socket, $this->timeout);
// Split Into Labels
if (\preg_match('/[a-z|A-Z]/', $question) == 0 && $question != '.') { // IP Address
// reverse ARPA format
$labels = \array_reverse(\explode('.', $question));
$labels[] = 'IN-ADDR';
$labels[] = 'ARPA';
} else {
if ($question == '.') {
$labels = [''];
} else { // hostname
$labels = \explode('.', $question);
}
}
$question_binary = '';
foreach ($labels as $label) {
if ($label != '') {
$size = \strlen($label);
$question_binary .= \pack('C', $size); // size byte first
$question_binary .= $label; // then the label
}
}
$question_binary .= pack('C', 0); // end it off
$this->debug('Question: ' . $question . ' (type=' . $type . '/' . $typeId . ')');
$id = \rand(1, 255) | (\rand(0, 255) << 8); // generate the ID
// Set standard codes and flags
$flags = 0x0100 & 0x0300; // recursion & queryspecmask
$opcode = 0x0000; // opcode
// Build the header
$header = '';
$header .= \pack('n', $id);
$header .= \pack('n', $opcode | $flags);
$header .= \pack('nnnn', 1, 0, 0, 0);
$header .= $question_binary;
$header .= \pack('n', $typeId);
$header .= \pack('n', 0x0001); // internet class
$headersize = \strlen($header);
$headersizebin = \pack('n', $headersize);
$this->debug('Header Length: ' . $headersize . ' Bytes');
$this->debugBinary($header);
if (($this->udp) && ($headersize >= 512)) {
$this->setError('Question too big for UDP (' . $headersize . ' bytes)');
\fclose($socket);
throw new \Exception('Question too big for UDP (' . $headersize . ' bytes)');
}
if ($this->udp) { // UDP method
if (!\fwrite($socket, $header, $headersize)) {
$this->setError('Failed to write question to socket');
\fclose($socket);
throw new \Exception('Failed to write question to socket');
}
if (!$this->rawBuffer = \fread($socket, 4096)) { // read until the end with UDP
$this->setError('Failed to read data buffer');
\fclose($socket);
throw new \Exception('Failed to read data buffer');
}
} else { // TCP
// write the socket
if (!\fwrite($socket, $headersizebin)) {
$this->setError('Failed to write question length to TCP socket');
\fclose($socket);
throw new \Exception('Failed to write question length to TCP socket');
}
if (!\fwrite($socket, $header, $headersize)) {
$this->setError('Failed to write question to TCP socket');
\fclose($socket);
throw new \Exception('Failed to write question to TCP socket');
}
if (!$returnsize = \fread($socket, 2)) {
$this->setError('Failed to read size from TCP socket');
\fclose($socket);
throw new \Exception('Failed to read size from TCP socket');
}
$tmplen = \unpack('nlength', $returnsize);
$datasize = $tmplen['length'];
$this->debug('TCP Stream Length Limit ' . $datasize);
if (!$this->rawBuffer = \fread($socket, $datasize)) {
$this->setError('Failed to read data buffer');
\fclose($socket);
throw new \Exception('Failed to read data buffer');
}
}
\fclose($socket);
$bufferSize = \strlen($this->rawBuffer);
$this->debug('Read Buffer Size ' . $bufferSize);
if ($bufferSize < 12) {
$this->setError('Return Buffer too Small');
throw new \Exception('Return Buffer too Small');
}
$this->rawHeader = \substr($this->rawBuffer, 0, 12); // first 12 bytes is the header
$this->rawResponse = \substr($this->rawBuffer, 12); // after that the response
$this->responseCounter = 12; // start parsing response counter from 12 - no longer using response so can do pointers
$this->debugBinary($this->rawBuffer);
$this->header = \unpack('nid/nspec/nqdcount/nancount/nnscount/narcount', $this->rawHeader);
$id = $this->header['id'];
$rcode = $this->header['spec'] & 15;
$ra = ($this->header['spec'] >> 7) & 1;
$rd = ($this->header['spec'] >> 8) & 1;
$tc = ($this->header['spec'] >> 9) & 1;
$aa = ($this->header['spec'] >> 10) & 1;
$opcode = ($this->header['spec'] >> 11) & 15;
$type = ($this->header['spec'] >> 15) & 1;
$this->debug("ID=$id, Type=$type, OPCODE=$opcode, AA=$aa, TC=$tc, RD=$rd, RA=$ra, RCODE=$rcode");
if ($tc == 1 && $this->udp) { // Truncation detected
$this->setError('Response too big for UDP, retry with TCP');
throw new \Exception('Response too big for UDP, retry with TCP');
}
$answers = $this->header['ancount'];
$this->debug('Query Returned ' . $answers . ' Answers');
$dns_answer = new Answer();
// Deal with the header question data
if ($this->header['qdcount'] > 0) {
$this->debug('Found ' . $this->header['qdcount'] . ' Questions');
for ($a = 0; $a < $this->header['qdcount']; $a++) {
$c = 1;
while ($c != 0) {
$c = \hexdec(\bin2hex($this->readResponse(1)));
}
$this->readResponse(4);
}
}
// New Functional Method
for ($a = 0; $a < $this->header['ancount']; $a++) {
$record = $this->readRecord();
$dns_answer->addResult(new Result($record['header']['type'], $record['typeId'],
$record['header']['class'], $record['header']['ttl'], $record['data'], $record['domain'],
$record['string'], $record['extras']));
}
$this->lastNameservers = new Answer();
for ($a = 0; $a < $this->header['nscount']; $a++) {
$record = $this->readRecord();
$this->lastNameservers->addResult(new Result($record['header']['type'], $record['typeId'],
$record['header']['class'], $record['header']['ttl'], $record['data'], $record['domain'],
$record['string'], $record['extras']));
}
$this->lastAdditional = new Answer();
for ($a = 0; $a < $this->header['arcount']; $a++) {
$record = $this->readRecord();
$this->lastAdditional->addResult(new Result($record['header']['type'], $record['typeId'],
$record['header']['class'], $record['header']['ttl'], $record['data'], $record['domain'],
$record['string'], $record['extras']));
}
return $dns_answer;
}
/**
* @param string $hostname
* @param int $depth
*
* @return string
*/
public function smartALookup($hostname, $depth = 0)
{
$this->debug('SmartALookup for ' . $hostname . ' depth ' . $depth);
// avoid recursive lookups
if ($depth > 5) {
return '';
}
// The SmartALookup function will resolve CNAMES using the additional properties if possible
$answer = $this->query($hostname, 'A');
// failed totally
if ($answer === false) {
return '';
}
// no records at all returned
if (count($answer) === 0) {
return '';
}
foreach ($answer as $record) {
// found it
if ($record->getTypeId() == 'A') {
$best_answer = $record;
break;
}
// alias
if ($record->getTypeId() == 'CNAME') {
$best_answer = $record;
// and keep going
}
}
if (!isset($best_answer)) {
return '';
}
if ($best_answer->getTypeId() == 'A') {
return $best_answer->getData();
} // got an IP ok
if ($best_answer->getTypeId() != 'CNAME') {
return '';
} // shouldn't ever happen
$newTarget = $best_answer->getData(); // this is what we now need to resolve
// First is it in the additional section
foreach ($this->lastAdditional as $result) {
if (($result->getDomain() == $hostname) && ($result->getTypeId() == 'A')) {
return $result->getData();
}
}
// Not in the results
return $this->smartALookup($newTarget, $depth + 1);
}
/**
* @return Answer
*/
public function getLastNameservers()
{
return $this->lastNameservers;
}
/**
* @return Answer
*/
public function getLastAdditional()
{
return $this->lastAdditional;
}
/**
* @return boolean
*/
public function hasError()
{
return $this->error;
}
/**
* @return string
*/
public function getLastError()
{
return $this->lastError;
}
/**
* @param int $count
* @param string $offset
*
* @return string
*/
private function readResponse($count = 1, $offset = '')
{
if ($offset == '') {
// no offset so use and increment the ongoing counter
$return = \substr($this->rawBuffer, $this->responseCounter, $count);
$this->responseCounter += $count;
} else {
$return = \substr($this->rawBuffer, $offset, $count);
}
return $return;
}
/**
* @param int $offset
* @param int $counter
*
* @return array
*/
private function readDomainLabels($offset, &$counter = 0)
{
$labels = [];
$startOffset = $offset;
$return = false;
while (!$return) {
$label_len = \ord($this->readResponse(1, $offset++));
if ($label_len <= 0) {
$return = true;
// end of data
} else {
if ($label_len < 64) { // uncompressed data
$labels[] = $this->readResponse($label_len, $offset);
$offset += $label_len;
} else { // label_len >= 64 -- pointer
$nextitem = $this->readResponse(1, $offset++);
$pointer_offset = (($label_len & 0x3f) << 8) + ord($nextitem);
// Branch Back Upon Ourselves...
$this->debug('Label Offset: ' . $pointer_offset);
$pointer_labels = $this->readDomainLabels($pointer_offset);
foreach ($pointer_labels as $ptr_label) {
$labels[] = $ptr_label;
}
$return = true;
}
}
}
$counter = $offset - $startOffset;
return $labels;
}
/**
* @return string
*/
private function readDomainLabel()
{
$count = 0;
$labels = $this->readDomainLabels($this->responseCounter, $count);
$domain = \implode('.', $labels);
$this->responseCounter += $count;
$this->debug('Label ' . $domain . ' len ' . $count);
return $domain;
}
/**
* @param string $text
*/
private function debug($text)
{
if ($this->debug) {
echo $text . "\n";
}
}
/**
* @param string $data
*/
private function debugBinary($data)
{
if (!$this->binaryDebug) {
return;
}
for ($a = 0; $a < \strlen($data); $a++) {
$hex = \bin2hex($data[$a]);
$dec = \hexdec($hex);
echo $a;
echo "\t";
printf('%d', $data[$a]);
echo "\t";
echo '0x' . $hex;
echo "\t";
echo $dec;
echo "\t";
if (($dec > 30) && ($dec < 150)) {
echo $data[$a];
}
echo "\n";
}
}
/**
* @param string $text
*
* @return $this
*/
private function setError($text)
{
$this->error = true;
$this->lastError = $text;
$this->debug('Error: ' . $text);
return $this;
}
/**
* @return $this
*/
private function clearError()
{
$this->error = false;
$this->lastError = '';
return $this;
}
/**
* @return array
*/
private function readRecord()
{
// First the pesky domain names - maybe not so pesky though I suppose
$domain = $this->readDomainLabel();
$ansHeaderBin = $this->readResponse(10); // 10 byte header
$ansHeader = \unpack('ntype/nclass/Nttl/nlength', $ansHeaderBin);
$this->debug('Record Type ' . $ansHeader['type'] . ' Class ' . $ansHeader['class'] . ' TTL ' . $ansHeader['ttl'] . ' Length ' . $ansHeader['length']);
$typeId = $this->types->getById($ansHeader['type']);
$extras = [];
$data = '';
$string = '';
switch ($typeId) {
case 'A':
$ipBin = $this->readResponse(4);
$ip = \inet_ntop($ipBin);
$data = $ip;
$extras['ipBin'] = $ipBin;
$string = $domain . ' has IPv4 address ' . $ip;
break;
case 'AAAA':
$ipBin = $this->readResponse(16);
$ip = \inet_ntop($ipBin);
$data = $ip;
$extras['ipBin'] = $ipBin;
$string = $domain . ' has IPv6 address ' . $ip;
break;
case 'CNAME':
$data = $this->readDomainLabel();
$string = $domain . ' alias of ' . $data;
break;
case 'DNAME':
$data = $this->readDomainLabel();
$string = $domain . ' alias of ' . $data;
break;
case 'DNSKEY':
case 'KEY':
$stuff = $this->readResponse(4);
// key type test 21/02/2014 DC
$test = \unpack('nflags/cprotocol/calgo', $stuff);
$extras['flags'] = $test['flags'];
$extras['protocol'] = $test['protocol'];
$extras['algorithm'] = $test['algo'];
$data = \base64_encode($this->readResponse($ansHeader['length'] - 4));
$string = $domain . ' KEY ' . $data;
break;
case "NSEC":
$data = $this->ReadDomainLabel();
$string = $domain . " points to " . $data;
break;
case 'MX':
$prefs = $this->readResponse(2);
$prefs = \unpack('nlevel', $prefs);
$extras['level'] = $prefs['level'];
$data = $this->readDomainLabel();
$string = $domain . ' mailserver ' . $data . ' (pri=' . $extras['level'] . ')';
break;
case 'NS':
$nameServer = $this->readDomainLabel();
$data = $nameServer;
$string = $domain . ' nameServer ' . $nameServer;
break;
case 'PTR':
$data = $this->readDomainLabel();
$string = $domain . ' points to ' . $data;
break;
case 'SOA':
// Label First
$data = $this->readDomainLabel();
$responsible = $this->readDomainLabel();
$buffer = $this->readResponse(20);
$extras = \unpack('Nserial/Nrefresh/Nretry/Nexpiry/Nminttl',
$buffer); // butfix to NNNNN from nnNNN for 1.01
$dot = \strpos($responsible, '.');
if ($dot !== false) {
$responsible[$dot] = '@';
}
$extras['responsible'] = $responsible;
$string = $domain . ' SOA ' . $data . ' Serial ' . $extras['serial'];
break;
case 'SRV':
$prefs = $this->readResponse(6);
$prefs = \unpack('npriority/nweight/nport', $prefs);
$extras['priority'] = $prefs['priority'];
$extras['weight'] = $prefs['weight'];
$extras['port'] = $prefs['port'];
$data = $this->readDomainLabel();
$string = $domain . ' SRV ' . $data . ':' . $extras['port'] . ' (pri=' . $extras['priority'] . ', weight=' . $extras['weight'] . ')';
break;
case 'TXT':
case 'SPF':
$data = '';
for ($string_count = 0; \strlen($data) + (1 + $string_count) < $ansHeader['length']; $string_count++) {
$string_length = \ord($this->readResponse(1));
$data .= $this->readResponse($string_length);
}
$string = $domain . ' TEXT "' . $data . '" (in ' . $string_count . ' strings)';
break;
case "NAPTR":
$buffer = $this->ReadResponse(4);
$extras = \unpack("norder/npreference", $buffer);
$addonitial = $this->ReadDomainLabel();
$data = $this->ReadDomainLabel();
$extras['service'] = $addonitial;
$string = $domain . " NAPTR " . $data;
break;
}
return [
'header' => $ansHeader,
'typeId' => $typeId,
'data' => $data,
'domain' => $domain,
'string' => $string,
'extras' => $extras,
];
}
}