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
2 changes: 1 addition & 1 deletion src/DNS/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ private function parseRdata(string $packet, int &$offset, int $type, int $rdleng
throw new Exception("Failed to parse SOA record.");
}
$offset += 20;
return "MNAME: {$mname}, RNAME: {$rname}, Serial: {$parts['serial']}, Refresh: {$parts['refresh']}, Retry: {$parts['retry']}, Expire: {$parts['expire']}, Minimum TTL: {$parts['minttl']}";
return "{$mname}. {$rname}. {$parts['serial']} {$parts['refresh']} {$parts['retry']} {$parts['expire']} {$parts['minttl']}";
default:
$data = substr($packet, $offset, $rdlength);
if ($data == false) {
Expand Down
59 changes: 58 additions & 1 deletion src/DNS/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,13 +228,14 @@ public function start(): void

$type = match ($typeByte) {
1 => 'A',
2 => 'NS',
5 => 'CNAME',
6 => 'SOA',
15 => 'MX',
16 => 'TXT',
28 => 'AAAA',
33 => 'SRV',
257 => 'CAA',
2 => 'NS',
default => 'A'
};

Expand Down Expand Up @@ -302,6 +303,7 @@ public function start(): void
'AAAA' => $this->encodeIPv6($answer->getRdata(), $answer->getTTL()),
'CNAME' => $this->encodeDomain($answer->getRdata(), $answer->getTTL()),
'NS' => $this->encodeDomain($answer->getRdata(), $answer->getTTL()),
'SOA' => $this->encodeSOA($answer->getRdata(), $answer->getTTL()),
'TXT' => $this->encodeText($answer->getRdata(), $answer->getTTL()),
'CAA' => $this->encodeCAA($answer->getRdata(), $answer->getTTL()),
'MX' => $this->encodeMx($answer->getRdata(), $answer->getTTL(), $answer->getPriority() ?? 0),
Expand Down Expand Up @@ -437,6 +439,61 @@ protected function encodeText(string $text, int $ttl): string
return $result;
}

/**
* Encode a SOA record according to RFC 1035.
* Format: MNAME RNAME SERIAL REFRESH RETRY EXPIRE MINIMUM
* @see https://datatracker.ietf.org/doc/html/rfc1035
*
* @param string $rdata SOA data in the format "mname rname serial refresh retry expire minimum"
* @param int $ttl
* @return string
*/
protected function encodeSOA(string $rdata, int $ttl): string
{
// Parse SOA record data: "ns1.example.com. admin.example.com. 2025011801 7200 3600 1209600 1800"
$parts = preg_split('/\s+/', trim($rdata));

if (!$parts || count($parts) < 7) {
throw new \Exception("Invalid SOA record format: {$rdata}");
}

$mname = $parts[0]; // Primary nameserver
$rname = $parts[1]; // Email address (with . instead of @)
$serial = (int)$parts[2]; // Serial number
$refresh = (int)$parts[3]; // Refresh interval
$retry = (int)$parts[4]; // Retry interval
$expire = (int)$parts[5]; // Expire time
$minimum = (int)$parts[6]; // Minimum TTL

// Encode MNAME (primary nameserver)
$mnameEncoded = '';
foreach (explode('.', rtrim($mname, '.')) as $label) {
$labelLength = strlen($label);
if ($labelLength > self::MAX_LABEL_LEN) {
throw new \Exception("Label too long in SOA MNAME: {$label}");
}
$mnameEncoded .= chr($labelLength) . $label;
}
$mnameEncoded .= chr(0);

// Encode RNAME (responsible person email)
$rnameEncoded = '';
foreach (explode('.', rtrim($rname, '.')) as $label) {
$labelLength = strlen($label);
if ($labelLength > self::MAX_LABEL_LEN) {
throw new \Exception("Label too long in SOA RNAME: {$label}");
}
$rnameEncoded .= chr($labelLength) . $label;
}
$rnameEncoded .= chr(0);

// Pack all SOA data
$soaData = $mnameEncoded . $rnameEncoded . pack('NNNNN', $serial, $refresh, $retry, $expire, $minimum);

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

/**
* Encode an MX record according to RFC 1035.
* @see https://datatracker.ietf.org/doc/html/rfc1035
Expand Down
28 changes: 28 additions & 0 deletions tests/DNS/ClientTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,4 +160,32 @@ public function testCAARecords(): void
$this->assertCount(1, $records);
$this->assertEquals('255 issuewild "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"', $records[0]->getRdata());
}

public function testSOARecords(): void
{
$records = $this->client->query('appwrite.io', 'SOA');

$this->assertCount(1, $records);
$this->assertEquals('appwrite.io', $records[0]->getName());
$this->assertEquals('IN', $records[0]->getClass());
$this->assertIsNumeric($records[0]->getTTL());
$this->assertEquals(3600, $records[0]->getTTL());
$this->assertEquals('SOA', $records[0]->getTypeName());

$rdata = $records[0]->getRdata();
$this->assertStringContainsString('ns1.appwrite.io.', $rdata);
$this->assertStringContainsString('admin.appwrite.io.', $rdata);
$this->assertStringContainsString('2025011801', $rdata);

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

$this->assertCount(1, $records);
$rdata = $records[0]->getRdata();
$this->assertStringContainsString('ns1.dev2.appwrite.io.', $rdata);
$this->assertStringContainsString('admin.dev2.appwrite.io.', $rdata);
$this->assertStringContainsString('2025011802', $rdata);

$records = $this->client->query('dev3.appwrite.io', 'SOA');
$this->assertCount(0, $records);
}
}
43 changes: 43 additions & 0 deletions tests/DNS/RecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -178,4 +178,47 @@ public function testSRVRecord(): void
$this->assertSame(20, $record->getWeight());
$this->assertSame(5060, $record->getPort());
}

/**
* Test a record configured for SOA (Start of Authority).
*/
public function testSOARecord(): void
{
$record = new Record();
$soaData = 'ns1.example.com. admin.example.com. 2025011801 7200 3600 1209600 1800';

$record->setName('example.com')
->setTTL(3600)
->setClass('IN')
->setType(6) // SOA numeric code is 6.
->setRdata($soaData);

$this->assertSame('example.com', $record->getName());
$this->assertSame(3600, $record->getTTL());
$this->assertSame('IN', $record->getClass());
$this->assertSame('SOA', $record->getTypeName());
$this->assertSame($soaData, $record->getRdata());
}

/**
* Test a record configured for SOA using string type.
*/
public function testSOARecordWithStringType(): void
{
$record = new Record();
$soaData = 'ns1.example.com. admin.example.com. 2025011801 7200 3600 1209600 1800';

$record->setName('example.com')
->setTTL(1800)
->setClass('IN')
->setType('SOA') // Using string instead of numeric code.
->setRdata($soaData);

$this->assertSame('example.com', $record->getName());
$this->assertSame(1800, $record->getTTL());
$this->assertSame('IN', $record->getClass());
$this->assertSame(6, $record->getType()); // Should convert to numeric 6
$this->assertSame('SOA', $record->getTypeName());
$this->assertSame($soaData, $record->getRdata());
}
}
9 changes: 9 additions & 0 deletions tests/DNS/ServerMemory.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@
'priority' => 10
]);

$resolver->addRecord('appwrite.io', 'SOA', [
'value' => 'ns1.appwrite.io. admin.appwrite.io. 2025011801 7200 3600 1209600 1800',
'ttl' => 3600
]);

$resolver->addRecord('dev2.appwrite.io', 'SOA', [
'value' => 'ns1.dev2.appwrite.io. admin.dev2.appwrite.io. 2025011802 14400 7200 2419200 3600'
]);

$dns = new Server($server, $resolver);
$dns->setDebug(false);

Expand Down
24 changes: 24 additions & 0 deletions tests/DNS/ZoneTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,30 @@ public function testValidateZeroTTL(): void
$this->assertEmpty($errors, 'Zero TTL should be accepted as valid integer TTL.');
}

public function testValidateSOARecordEmptyData(): void
{
$z = new Zone();
$domain = 'example.com';

// Test with completely missing SOA data
$zoneFile = "example.com. 3600 IN SOA";

$errors = $z->validate($domain, $zoneFile);
$this->assertNotEmpty($errors, 'Expected validation error for SOA with empty data');
$this->assertStringContainsString('SOA record has empty data', $errors[0]);
}

public function testValidateSOARecordValidData(): void
{
$z = new Zone();
$domain = 'example.com';

$zoneFile = "@ IN SOA ns1.example.com. admin.example.com. 2025011801 7200 3600 1209600 1800\n";

$errors = $z->validate($domain, $zoneFile);
$this->assertEmpty($errors, 'Valid SOA record should not produce errors.');
}

public function testImportWithDirectivesAndAutoQualification(): void
{
$z = new Zone();
Expand Down