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
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Utopia\DNS\Exception;
namespace Utopia\DNS\Exception\Message;

/**
* Exception thrown when a DNS message is not decoded.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?php

namespace Utopia\DNS\Exception;
namespace Utopia\DNS\Exception\Message;

use Utopia\DNS\Message;
use Utopia\DNS\Message\Header;
Expand Down
25 changes: 25 additions & 0 deletions src/DNS/Exception/Zone/ImportException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Utopia\DNS\Exception\Zone;

/**
* Exception thrown during Zone file import.
*/
final class ImportException extends \RuntimeException
{
public function __construct(
private readonly string $content,
string $message = '',
?\Throwable $previous = null
) {
parent::__construct($message, previous: $previous);
}

/**
* Returns the content of the Zone file.
*/
public function getContent(): string
{
return $this->content;
}
}
4 changes: 2 additions & 2 deletions src/DNS/Message.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Utopia\DNS;

use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\PartialDecodingException;
use Utopia\DNS\Exception\Message\DecodingException;
use Utopia\DNS\Exception\Message\PartialDecodingException;
use Utopia\DNS\Message\Header;
use Utopia\DNS\Message\Question;
use Utopia\DNS\Message\Record;
Expand Down
2 changes: 1 addition & 1 deletion src/DNS/Message/Domain.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Utopia\DNS\Message;

use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\Message\DecodingException;

final readonly class Domain
{
Expand Down
2 changes: 1 addition & 1 deletion src/DNS/Message/Header.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Utopia\DNS\Message;

use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\Message\DecodingException;

final readonly class Header
{
Expand Down
2 changes: 1 addition & 1 deletion src/DNS/Message/Question.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Utopia\DNS\Message;

use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\Message\DecodingException;

final readonly class Question
{
Expand Down
2 changes: 1 addition & 1 deletion src/DNS/Message/Record.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Utopia\DNS\Message;

use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\Message\DecodingException;

/**
* A DNS record.
Expand Down
2 changes: 1 addition & 1 deletion src/DNS/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use Throwable;
use Utopia\Console;
use Utopia\DNS\Exception\PartialDecodingException;
use Utopia\DNS\Exception\Message\PartialDecodingException;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\Telemetry\Counter;
Expand Down
8 changes: 4 additions & 4 deletions src/DNS/Validator/DNS.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@

class DNS extends Validator
{
protected const FAILURE_REASON_QUERY = 'DNS query failed.';
protected const FAILURE_REASON_INTERNAL = 'Internal error occurred.';
protected const FAILURE_REASON_UNKNOWN = '';
protected const DEFAULT_DNS_SERVER = '8.8.8.8';
protected const string FAILURE_REASON_QUERY = 'DNS query failed.';
protected const string FAILURE_REASON_INTERNAL = 'Internal error occurred.';
protected const string FAILURE_REASON_UNKNOWN = '';
protected const string DEFAULT_DNS_SERVER = '8.8.8.8';

// Memory from isValid to be used in getDescription
/**
Expand Down
41 changes: 23 additions & 18 deletions src/DNS/Zone/File.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
namespace Utopia\DNS\Zone;

use InvalidArgumentException;
use Utopia\DNS\Exception\Zone\ImportException;
use Utopia\DNS\Message\Record;
use Utopia\DNS\Zone;

Expand Down Expand Up @@ -31,7 +32,7 @@
* @param string|null $defaultOrigin Default origin if $ORIGIN is not specified
* @param int $defaultTTL Default TTL if not specified (default: 3600)
*
* @throws InvalidArgumentException
* @throws ImportException
*/
public static function import(string $content, ?string $defaultOrigin = null, int $defaultTTL = 3600): Zone
{
Expand All @@ -46,7 +47,7 @@ public static function import(string $content, ?string $defaultOrigin = null, in
if ($defaultOrigin !== null) {
$origin = self::canonicalizeName($defaultOrigin);
if ($origin === null) {
throw new InvalidArgumentException('Default origin must not be empty');
throw new ImportException($content, 'Default origin must not be empty');
}
$zoneName = $origin;
$zoneNameFromDefault = true;
Expand All @@ -62,11 +63,16 @@ public static function import(string $content, ?string $defaultOrigin = null, in
}

// Directives
$directive = self::handleDirectives($line, $origin, $lastTTL);
try {
$directive = self::handleDirectives($line, $origin, $lastTTL);
} catch (InvalidArgumentException $e) {
throw new ImportException($content, $e->getMessage(), previous: $e);
}

if ($directive !== null) {
if ($directive === 'origin') {
if ($origin === null) {
throw new InvalidArgumentException('$ORIGIN directive must not be empty');
throw new ImportException($content, '$ORIGIN directive must not be empty');
}
if ($zoneName === null || $zoneNameFromDefault) {
$zoneName = $origin;
Expand All @@ -84,7 +90,11 @@ public static function import(string $content, ?string $defaultOrigin = null, in
$ownerOmitted = $line[0] === ' ' || $line[0] === "\t";
$line = ltrim($line);

$rr = self::parseResourceRecord($line, $origin, $lastOwner, $lastTTL, $lastClass, $ownerOmitted, $num);
try {
$rr = self::parseResourceRecord($line, $origin, $lastOwner, $lastTTL, $lastClass, $ownerOmitted, $num);
} catch (InvalidArgumentException $e) {
throw new ImportException($content, $e->getMessage(), previous: $e);
}

// Update state from parsed RR
$lastOwner = $rr['name'];
Expand All @@ -104,7 +114,7 @@ class: $rr['class'],

if ($rr['type'] === Record::TYPE_SOA) {
if ($soa !== null) {
throw new InvalidArgumentException("Multiple SOA records found (line $num).");
throw new ImportException($content, "Multiple SOA records found (line $num).");
}
$soa = $record;
continue;
Expand All @@ -114,11 +124,11 @@ class: $rr['class'],
}

if ($soa === null) {
throw new InvalidArgumentException('No SOA record found in zone file');
throw new ImportException($content, 'No SOA record found in zone file');
}

if ($zoneName === null) {
throw new InvalidArgumentException('Unable to determine zone name: provide an $ORIGIN directive or defaultOrigin.');
throw new ImportException($content, 'Unable to determine zone name: provide an $ORIGIN directive or defaultOrigin.');
}

return new Zone($zoneName, $records, $soa);
Expand Down Expand Up @@ -357,7 +367,11 @@ private static function parseResourceRecord(
}

$typeString = strtoupper($tokens[$i]);
$type = self::parseType($typeString);
$type = Record::typeNameToCode($typeString);
if ($type === null) {
throw new InvalidArgumentException("Invalid record type '$typeString' (line $lineNum).");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional, or ImportException too?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Intentional, it's within a static method that doesn't have the $content variable in scope.

We throw this exception and it's caught by a try catch in the import() method 👍

}

$i++;

$rdataTokens = array_slice($tokens, $i);
Expand Down Expand Up @@ -598,15 +612,6 @@ private static function relativizeDomainName(string $name, string $origin): stri
return $name;
}

private static function parseType(string $typeString): int
{
$type = Record::typeNameToCode($typeString);
if ($type === null) {
throw new InvalidArgumentException("Unknown record type '$typeString'");
}
return $type;
}

private static function getTypeString(int $type): string
{
return Record::typeCodeToName($type) ?? 'TYPE' . $type;
Expand Down
2 changes: 1 addition & 1 deletion tests/unit/DNS/Message/DomainTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Tests\Unit\Utopia\DNS\Message;

use PHPUnit\Framework\TestCase;
use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\Message\DecodingException;
use Utopia\DNS\Message\Domain;
use PHPUnit\Framework\Attributes\DataProvider;

Expand Down
2 changes: 1 addition & 1 deletion tests/unit/DNS/Message/HeaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace Tests\Unit\Utopia\DNS\Message;

use PHPUnit\Framework\TestCase;
use Utopia\DNS\Exception\DecodingException;
use Utopia\DNS\Exception\Message\DecodingException;
use Utopia\DNS\Message\Header;

final class HeaderTest extends TestCase
Expand Down
10 changes: 6 additions & 4 deletions tests/unit/DNS/MessageTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Tests\Unit\Utopia\DNS;

use PHPUnit\Framework\TestCase;
use Utopia\DNS\Exception\Message\DecodingException;
use Utopia\DNS\Exception\Message\PartialDecodingException;
use Utopia\DNS\Message;
use Utopia\DNS\Message\Header;
use Utopia\DNS\Message\Record;
Expand Down Expand Up @@ -189,7 +191,7 @@ public function testDecodeThrowsWhenPacketTooShort(): void
{
$packet = "\x00\x01\x00"; // shorter than 12-byte DNS header

$this->expectException(\Utopia\DNS\Exception\DecodingException::class);
$this->expectException(DecodingException::class);
$this->expectExceptionMessage('Invalid DNS response: header too short');

Message::decode($packet);
Expand All @@ -206,7 +208,7 @@ public function testDecodeThrowsPartialDecodingOnTruncatedQuestion(): void
try {
Message::decode($packet);
$this->fail('Expected PartialDecodingException');
} catch (\Utopia\DNS\Exception\PartialDecodingException $e) {
} catch (PartialDecodingException $e) {
$header = $e->getHeader();
$this->assertSame(0x1234, $header->id);
$this->assertSame(1, $header->questionCount);
Expand All @@ -229,7 +231,7 @@ public function testDecodeThrowsPartialDecodingOnTruncatedAnswer(): void
try {
Message::decode($header . $question . $answer);
$this->fail('Expected PartialDecodingException');
} catch (\Utopia\DNS\Exception\PartialDecodingException $e) {
} catch (PartialDecodingException $e) {
$this->assertSame(0xABCD, $e->getHeader()->id);
$this->assertSame('RDATA exceeds packet bounds', $e->getMessage());
}
Expand All @@ -249,7 +251,7 @@ public function testDecodeThrowsPartialDecodingOnExtraBytes(): void
"\x5D\xB8\xD8\x22" .
"\xFF"; // extra byte

$this->expectException(\Utopia\DNS\Exception\PartialDecodingException::class);
$this->expectException(PartialDecodingException::class);
$this->expectExceptionMessage('Invalid packet length');

Message::decode($message);
Expand Down
28 changes: 14 additions & 14 deletions tests/unit/DNS/Zone/FileTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

namespace Tests\Utopia\DNS\Zone;

use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
use Utopia\DNS\Exception\Zone\ImportException;
use Utopia\DNS\Message\Record;
use Utopia\DNS\Zone;
use Utopia\DNS\Zone\File;
Expand Down Expand Up @@ -103,7 +103,7 @@ public function testImportValidZoneWithDirectives(): void

public function testImportFailsWithUnsupportedDirective(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('$INCLUDE directive is not supported');

File::import(<<<ZONE
Expand All @@ -114,8 +114,8 @@ public function testImportFailsWithUnsupportedDirective(): void

public function testImportFailsWithUnknownRecordType(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("Unknown record type 'BADTYPE'");
$this->expectException(ImportException::class);
$this->expectExceptionMessage("Invalid record type 'BADTYPE' (line 3).");

$soa = self::DEFAULT_SOA;
$contents = <<<ZONE
Expand All @@ -129,7 +129,7 @@ public function testImportFailsWithUnknownRecordType(): void

public function testImportFailsWhenMxPriorityMissing(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('MX requires numeric priority and exchange');

$soa = self::DEFAULT_SOA;
Expand All @@ -144,7 +144,7 @@ public function testImportFailsWhenMxPriorityMissing(): void

public function testImportFailsWhenSrvFieldsMissing(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('SRV requires priority, weight, port, target');

$soa = self::DEFAULT_SOA;
Expand Down Expand Up @@ -204,7 +204,7 @@ public function testImportUsesDefaultOriginWhenDirectiveMissing(): void

public function testImportFailsWhenSoaDataMissing(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('SOA requires MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM');

File::import('@ IN SOA ns1.example.com. admin.example.com.', 'example.com');
Expand Down Expand Up @@ -592,7 +592,7 @@ public function testImportExportRoundTripForCaa(): void

public function testImportCaaMissingQuotedValueFails(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessageMatches('/CAA value must be quoted/');

$contents = sprintf(
Expand Down Expand Up @@ -623,7 +623,7 @@ public function testImportPtrWithReverseOrigin(): void

public function testImportFailsWithDuplicateSoa(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('Multiple SOA records found');

$contents = <<<'ZONE'
Expand All @@ -637,8 +637,8 @@ public function testImportFailsWithDuplicateSoa(): void

public function testImportRejectsTtlWithSuffix(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage("Unknown record type '1H'");
$this->expectException(ImportException::class);
$this->expectExceptionMessage("Invalid record type '1H' (line 3).");

$contents = sprintf(
<<<'ZONE'
Expand Down Expand Up @@ -705,23 +705,23 @@ public function testImportTxtThreeDigitEscapeConsumesOnlyThreeDigits(): void

public function testImportFailsWhenSoaHasTooFewFields(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('SOA requires MNAME, RNAME, SERIAL, REFRESH, RETRY, EXPIRE, MINIMUM');

File::import('@ IN SOA ns1.example.com. admin.example.com. 2025011801 7200 3600', 'example.com');
}

public function testImportFailsWithoutSoa(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('No SOA record found in zone file');

File::import("www IN A 192.168.1.10\n", 'example.com');
}

public function testImportFailsWhenOwnerOmittedWithoutContext(): void
{
$this->expectException(InvalidArgumentException::class);
$this->expectException(ImportException::class);
$this->expectExceptionMessage('Owner omitted but no previous owner available');

File::import(<<<ZONE
Expand Down