Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
117 changes: 117 additions & 0 deletions src/DNS/Validator/CAA.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

namespace Utopia\DNS\Validator;

use Utopia\Validator;

Comment thread
coderabbitai[bot] marked this conversation as resolved.
class CAA extends Validator
{
protected const int CAA_FLAG_MIN = 0;

protected const int CAA_FLAG_MAX = 255;

protected const string FAILURE_REASON_INVALID_FLAGS = 'Invalid flags';

protected const string FAILURE_REASON_INVALID_TAG = 'Invalid tag';

protected const string FAILURE_REASON_INVALID_VALUE = 'Invalid value';

protected const string FAILURE_REASON_INVALID_FORMAT = 'Invalid format';

public string $reason = '';

/**
* Check if the provided value matches the CAA record format
*
* @param mixed $value
* @return bool
*/
Comment thread
hmacr marked this conversation as resolved.
public function isValid(mixed $data): bool
{
if (!is_string($data)) {
$this->reason = self::FAILURE_REASON_INVALID_FORMAT;
return false;
}

$parts = explode(" ", $data, 3);

if (count($parts) !== 3) {
$this->reason = self::FAILURE_REASON_INVALID_FORMAT;
return false;
}

$flags = $parts[0];
$tag = $parts[1];
$value = $parts[2];

// Check flags is a number
if (!is_numeric($flags)) {
$this->reason = self::FAILURE_REASON_INVALID_FLAGS;
return false;
}

$flags = (int) $flags;

// Check flags is within the allowed range
if ($flags < self::CAA_FLAG_MIN || $flags > self::CAA_FLAG_MAX) {
$this->reason = self::FAILURE_REASON_INVALID_FLAGS;
return false;
}

// Check tag is not empty
if (strlen($tag) === 0) {
$this->reason = self::FAILURE_REASON_INVALID_TAG;
return false;
}

// Check value is not empty and starts with " and ends with "
if (strlen($value) === 0 || $value[0] !== '"' || $value[strlen($value) - 1] !== '"') {
$this->reason = self::FAILURE_REASON_INVALID_VALUE;
return false;
}

$value = substr($value, 1, strlen($value) - 2);

// Check value is not empty after removing the quotes
if (strlen($value) === 0) {
$this->reason = self::FAILURE_REASON_INVALID_VALUE;
return false;
}

// All checks passed
return true;
}

public function getDescription(): string
{
$messages = ['CAA verification failed'];

$messages[] = !empty($this->reason) ? $this->reason : self::FAILURE_REASON_INVALID_FORMAT;

return implode('. ', $messages) . '.';
}

/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
public function isArray(): bool
{
return false;
}

/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
}
}
81 changes: 81 additions & 0 deletions src/DNS/Validator/Name.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace Utopia\DNS\Validator;

use Utopia\DNS\Message\Domain;
use Utopia\Validator;

class Name extends Validator
{
public function isValid(mixed $name): bool
{
if (!is_string($name)) {
return false;
}

// DNS names are made up of labels separated by dots.
// Each label: 1-63 chars, letters, digits, hyphens, can't start/end w/ hyphen.
// Full name: <=255 chars, labels separated by single dots, no empty labels unless root.

if (strlen($name) < 1 || strlen($name) > Domain::MAX_DOMAIN_NAME_LEN) {
return false;
}

// If the name ends with '.', strip it (absolute FQDN); allow trailing '.'.
$trimmed = (substr($name, -1) === '.') ? substr($name, 0, -1) : $name;

$labels = explode('.', $trimmed);

// Disallow empty label except root "." (which means $trimmed = '')
foreach ($labels as $label) {
if ($label === '' || strlen($label) > 63 || strlen($label) < 1) {
Comment thread
hmacr marked this conversation as resolved.
Outdated
return false;
}
// RFC: Only a-z 0-9 -, can't start or end with '-'

// Check first and last character are alphanumeric
$len = strlen($label);
if (
$len < 1 ||
!ctype_alnum($label[0]) ||
!ctype_alnum($label[$len - 1])
) {
return false;
}
Comment thread
hmacr marked this conversation as resolved.
Outdated

// Check label contains only allowed chars
for ($i = 0; $i < $len - 1; ++$i) {
$c = $label[$i];
if (!ctype_alnum($c) && $c !== '-') {
return false;
}
}
}

return true;
}

/**
* @inheritDoc
*/
public function getDescription(): string
{
return 'Invalid name for DNS record';
}

/**
* @inheritDoc
*/
public function getType(): string
{
return self::TYPE_STRING;
}

/**
* @inheritDoc
*/
public function isArray(): bool
{
return false;
}
}
44 changes: 44 additions & 0 deletions tests/unit/DNS/Validator/CAATest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Tests\Unit\Utopia\DNS\Validator;

use PHPUnit\Framework\TestCase;
use Utopia\DNS\Validator\CAA;

final class CAATest extends TestCase
{
public function testValid(): void
{
$validator = new CAA();

$validValues = [
'0 issue "letsencrypt.org"',
'128 issuewild "certainly.com;account=123456;validationmethods=dns-01"',
'0 issuewild "certainly.com"',
'0 iodef "mailto:security@example.com"',
'0 issue ";"',
'0 issue "certainly.com; validationmethods=dns-01"',
];
Comment thread
hmacr marked this conversation as resolved.

foreach ($validValues as $value) {
$this->assertTrue($validator->isValid($value), "Expected valid: {$value}");
}
}

public function testInvalid(): void
{
$validator = new CAA();

$invalidValues = [
['value' => 'issue "letsencrypt.org"', 'description' => 'Invalid format'],
['value' => '256 issue "letsencrypt.org"', 'description' => 'Invalid flags'],
['value' => '0 issue letsencrypt.org', 'description' => 'Invalid value'],
['value' => '0 issue ""', 'description' => 'Invalid value'],
];

foreach ($invalidValues as $invalidValue) {
$this->assertFalse($validator->isValid($invalidValue['value']), "Expected invalid: {$invalidValue['value']}");
$this->assertSame("CAA verification failed. {$invalidValue['description']}.", $validator->getDescription());
}
}
}
55 changes: 55 additions & 0 deletions tests/unit/DNS/Validator/NameTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Tests\Unit\Utopia\DNS\Validator;

use PHPUnit\Framework\TestCase;
use Utopia\DNS\Validator\Name;

final class NameTest extends TestCase
{
public function testValid(): void
{
$validator = new Name();

$validValues = [
'example',
'example.com',
'EXAMPLE.COM',
'a-b.com',
'a123.example-domain.org',
'xn--d1acufc.xn--p1ai',
'123.com',
'example.com.',
str_repeat('a', 63) . '.com',
];

foreach ($validValues as $value) {
$this->assertTrue($validator->isValid($value), "Expected valid: {$value}");
}
}

public function testInvalid(): void
{
$validator = new Name();

$invalidValues = [
'',
'-example.com',
'example-.com',
'exa_mple.com',
'example..com',
str_repeat('a', 64) . '.com',
123,
'.example.com',
'example.com..',
'exa mple.com',
];

foreach ($invalidValues as $value) {
$this->assertFalse($validator->isValid($value), 'Expected invalid value');
$this->assertSame('Invalid name for DNS record', $validator->getDescription());
}

}
}

Loading