Skip to content
5 changes: 3 additions & 2 deletions src/DNS/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,11 @@ private function parseRdata(string $packet, int &$offset, int $type, int $rdleng
while ($offset < $end) {
$len = ord($packet[$offset]);
$offset++;
$txts[] = substr($packet, $offset, $len);
$chunk = ($len > 0) ? substr($packet, $offset, $len) : '';
$txts[] = $chunk;
$offset += $len;
}
return implode(' ', $txts);
return implode('', $txts);
case 33: // SRV record
$priority = unpack('n', substr($packet, $offset, 2));
$weight = unpack('n', substr($packet, $offset + 2, 2));
Expand Down
151 changes: 103 additions & 48 deletions src/DNS/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -330,100 +330,155 @@ protected function resolve(array $question): array
return $this->resolver->resolve($question);
}

/**
* Encode an IPv4 address (A record) according to RFC 1035.
*
* @param string $ip
* @param int $ttl
* @return string
*/
protected function encodeIP(string $ip, int $ttl): string
{
$result = \pack('Nn', $ttl, 4);

$binaryIP = inet_pton($ip);
if ($binaryIP === false) {
if ($binaryIP === false || strlen($binaryIP) !== 4) {
throw new \Exception("Invalid IPv4 address format: {$ip}");
}

// Append the binary IPv4 address directly
$result .= $binaryIP;

$result = pack('Nn', $ttl, 4) . $binaryIP;
Comment thread
lohanidamodar marked this conversation as resolved.
Outdated
return $result;
}

/**
* Encode an IPv6 address (AAAA record) according to RFC 3596.
*
* @param string $ip
* @param int $ttl
* @return string
*/
protected function encodeIPv6(string $ip, int $ttl): string
{
$result = \pack('Nn', $ttl, 16);

$binaryIP = inet_pton($ip);
if ($binaryIP === false) {
if ($binaryIP === false || strlen($binaryIP) !== 16) {
throw new \Exception("Invalid IPv6 address format: {$ip}");
}

$result .= $binaryIP;

$result = pack('Nn', $ttl, 16) . $binaryIP;
return $result;
}

/**
* Encode a domain name (CNAME, NS, PTR) according to RFC 1035.
*
* @param string $domain
* @param int $ttl
* @return string
*/
protected function encodeDomain(string $domain, int $ttl): string
{
$labels = explode('.', rtrim($domain, '.'));
$result = '';
$totalLength = 0;

foreach (\explode('.', $domain) as $label) {
$labelLength = \strlen($label);
$result .= \chr($labelLength);
$result .= $label;
foreach ($labels as $label) {
$labelLength = strlen($label);
if ($labelLength === 0) {
throw new \Exception("Empty label in domain: '{$domain}'");
}
if ($labelLength > 63) {
throw new \Exception("Label too long in domain: {$label}");
}
$result .= chr($labelLength) . $label;
$totalLength += 1 + $labelLength;
}

$result .= \chr(0);
$result .= chr(0);
$totalLength += 1;

$result = \pack('Nn', $ttl, $totalLength) . $result;

$result = pack('Nn', $ttl, $totalLength) . $result;
return $result;
Comment thread
lohanidamodar marked this conversation as resolved.
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Encode a TXT record according to RFC 1035.
*
* @param string $text
* @param int $ttl
* @return string
*/
protected function encodeText(string $text, int $ttl): string
{
$textLength = \strlen($text);
$result = \pack('Nn', $ttl, 1 + $textLength) . \chr($textLength) . $text;

$chunks = [];
$len = strlen($text);
for ($i = 0; $i < $len; $i += 255) {
$chunk = substr($text, $i, 255);
$chunks[] = chr(strlen($chunk)) . $chunk;
}
$txtData = implode('', $chunks);
$result = pack('Nn', $ttl, strlen($txtData)) . $txtData;
return $result;
}

/**
* Encode an MX record according to RFC 1035.
*
* @param string $domain
* @param int $ttl
* @param int $priority
* @return string
*/
protected function encodeMx(string $domain, int $ttl, int $priority): string
{
$result = \pack('n', $priority);
$labels = explode('.', rtrim($domain, '.'));
$result = pack('n', $priority);
$totalLength = 2;

foreach (\explode('.', $domain) as $label) {
$labelLength = \strlen($label);
$result .= \chr($labelLength);
$result .= $label;
foreach ($labels as $label) {
$labelLength = strlen($label);
if ($labelLength > 63) {
throw new \Exception("Label too long in MX domain: {$label}");
}
$result .= chr($labelLength) . $label;
$totalLength += 1 + $labelLength;
}

$result .= \chr(0);
$result .= chr(0);
$totalLength += 1;

$result = \pack('Nn', $ttl, $totalLength) . $result;

$result = pack('Nn', $ttl, $totalLength) . $result;
return $result;
Comment thread
lohanidamodar marked this conversation as resolved.
}
Comment thread
lohanidamodar marked this conversation as resolved.

/**
* Encode an SRV record according to RFC 2782.
*
* @param string $domain
* @param int $ttl
* @param int $priority
* @param int $weight
* @param int $port
* @return string
*/
protected function encodeSrv(string $domain, int $ttl, int $priority, int $weight, int $port): string
{
$result = \pack('nnn', $priority, $weight, $port);
// Validate SRV parameters
if ($priority < 0 || $priority > 65535) {
throw new \Exception("SRV priority out of range: {$priority}");
}
if ($weight < 0 || $weight > 65535) {
throw new \Exception("SRV weight out of range: {$weight}");
}
if ($port < 0 || $port > 65535) {
throw new \Exception("SRV port out of range: {$port}");
}
$labels = explode('.', rtrim($domain, '.'));
$result = pack('nnn', $priority, $weight, $port);
$totalLength = 6;

foreach (\explode('.', $domain) as $label) {
$labelLength = \strlen($label);
$result .= \chr($labelLength);
$result .= $label;
foreach ($labels as $label) {
$labelLength = strlen($label);
if ($labelLength === 0) {
throw new \Exception("Empty label in SRV domain: '{$domain}'");
}
if ($labelLength > 63) {
throw new \Exception("Label too long in SRV domain: {$label}");
}
$result .= chr($labelLength) . $label;
$totalLength += 1 + $labelLength;
}

$result .= \chr(0);
$result .= chr(0);
$totalLength += 1;

$result = \pack('Nn', $ttl, $totalLength) . $result;

$result = pack('Nn', $ttl, $totalLength) . $result;
return $result;
Comment thread
lohanidamodar marked this conversation as resolved.
}
Comment thread
lohanidamodar marked this conversation as resolved.

Expand Down
46 changes: 39 additions & 7 deletions src/DNS/Zone.php
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,14 @@ public function import(string $domain, string $content): array
}
$type = strtoupper($tokens[$pos++]);
$dataParts = array_slice($tokens, $pos);
$data = implode(' ', $dataParts);
if ($type === 'TXT') {
$data = '';
foreach ($dataParts as $part) {
$data .= $part;
}
} else {
$data = implode(' ', $dataParts);
}
if ($owner !== '@' && !str_ends_with($owner, '.')) {
$owner .= '.' . rtrim($this->defaultOrigin, '.');
}
Expand Down Expand Up @@ -360,22 +367,47 @@ protected function isComment(string $line): bool
*
* @return string[]
*/
/**
* Tokenize a line by whitespace, handling quoted strings and escapes per RFC 1035.
*
* @return string[]
*/
protected function tokenize(string $line): array
{
$tokens = [];
$current = '';
$inSpace = true;
$inQuote = false;
$escape = false;
for ($i = 0, $len = strlen($line); $i < $len; $i++) {
$c = $line[$i];
if ($c === ' ' || $c === "\t") {
if (!$inSpace) {
if ($escape) {
$current .= $c;
$escape = false;
continue;
}
if ($c === '\\') {
$escape = true;
continue;
}
if ($inQuote) {
if ($c === '"') {
$inQuote = false;
$tokens[] = $current;
$current = '';
} else {
$current .= $c;
}
$inSpace = true;
} else {
$current .= $c;
$inSpace = false;
if ($c === '"') {
$inQuote = true;
} elseif ($c === ' ' || $c === "\t") {
if ($current !== '') {
$tokens[] = $current;
$current = '';
}
} else {
$current .= $c;
}
}
}
if ($current !== '') {
Expand Down
3 changes: 2 additions & 1 deletion tests/DNS/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,10 @@ public function testTXTRecords(): void

$records = $this->client->query('dev2.appwrite.io', 'TXT');

$this->assertCount(2, $records);
$this->assertCount(3, $records);
$this->assertEquals('key with "$\'- symbols', $records[0]->getRdata());
$this->assertEquals('key with spaces', $records[1]->getRdata());
$this->assertEquals('v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;', $records[2]->getRdata());

$records = $this->client->query('dev3.appwrite.io', 'TXT');
$this->assertCount(0, $records);
Expand Down
4 changes: 4 additions & 0 deletions tests/DNS/ServerMemory.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@
'value' => 'key with spaces'
]);

$resolver->addRecord('dev2.appwrite.io', 'TXT', [
'value' => 'v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;'
]);

$resolver->addRecord('dev.appwrite.io', 'CAA', [
'value' => 'issue "letsencrypt.org"',
'ttl' => 50
Expand Down
13 changes: 13 additions & 0 deletions tests/DNS/ZoneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -337,4 +337,17 @@ public function testImportExportRoundTrip(): void
$this->assertSame($records1[2]->getPriority(), $records2[2]->getPriority());
$this->assertSame($records1[2]->getRdata(), $records2[2]->getRdata());
}

public function testImportTxtWithSpecialChars()
{
$zone = new Zone();
$zonefile = <<<ZONE
@ 3600 IN TXT "v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;"
ZONE;
$records = $zone->import('example.com', $zonefile);
$this->assertCount(1, $records);
$rec = $records[0];
$this->assertEquals('TXT', $rec->getTypeName());
$this->assertEquals('v=DMARC1; p=none; rua=mailto:jon@snow.got; ruf=mailto:jon@snow.got; fo=1;', trim($rec->getRdata(), '"'));
}
}
Loading