diff --git a/composer.json b/composer.json index 62abce9ca2..aef47daf85 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "utopia-php/detector": "0.2.*", "utopia-php/domains": "0.9.*", "utopia-php/emails": "0.6.*", - "utopia-php/dns": "1.1.*", + "utopia-php/dns": "1.3.*", "utopia-php/dsn": "0.2.1", "utopia-php/framework": "0.33.*", "utopia-php/fetch": "0.4.*", diff --git a/composer.lock b/composer.lock index bf75e2935b..d57b0fe5bc 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ad28b7155175986191bd19bbcd13d623", + "content-hash": "001835bef99a3dc73ab54cf4b22fc5e8", "packages": [ { "name": "adhocore/jwt", @@ -3947,16 +3947,16 @@ }, { "name": "utopia-php/dns", - "version": "1.1.3", + "version": "1.3.0", "source": { "type": "git", "url": "https://github.com/utopia-php/dns.git", - "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707" + "reference": "7644276913f68648515228d68684feca8742a645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/dns/zipball/1e6b4bac735329c9e5ec69a6a5d899ec2d050707", - "reference": "1e6b4bac735329c9e5ec69a6a5d899ec2d050707", + "url": "https://api.github.com/repos/utopia-php/dns/zipball/7644276913f68648515228d68684feca8742a645", + "reference": "7644276913f68648515228d68684feca8742a645", "shasum": "" }, "require": { @@ -3998,9 +3998,9 @@ ], "support": { "issues": "https://github.com/utopia-php/dns/issues", - "source": "https://github.com/utopia-php/dns/tree/1.1.3" + "source": "https://github.com/utopia-php/dns/tree/1.3.0" }, - "time": "2025-11-06T19:08:29+00:00" + "time": "2025-11-12T19:24:16+00:00" }, { "name": "utopia-php/domains", diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php new file mode 100644 index 0000000000..e6c05c1892 --- /dev/null +++ b/src/Appwrite/Network/Validator/DNS.php @@ -0,0 +1,94 @@ + + */ + protected array $dnsServers = []; + + /** + * @param string $target Expected value for the DNS record + * @param int $type Type of DNS record to validate + * For value, use const from Record, such as Record::TYPE_A + * When using CAA type, you can provide exact match, or just issuer domain as $target + * @param string|array $dnsServers DNS server IP(s) or domain(s) to use for validation + */ + public function __construct(string $target, int $type = Record::TYPE_CNAME, string|array $dnsServers = self::DEFAULT_DNS_SERVER) + { + parent::__construct($target, $type, is_array($dnsServers) ? $dnsServers[0] : $dnsServers); + + $this->dnsServers = is_array($dnsServers) ? $dnsServers : [$dnsServers]; + } + + /** + * Validate DNS record value against multiple DNS servers + * + * @param mixed $value + * @return bool + */ + public function isValid(mixed $value): bool + { + if (\count($this->dnsServers) === 1) { + return $this->isValidWithDNSServer($value, $this->dnsServers[0]); + } + + $wg = new WaitGroup(); + $failedValidator = null; + + foreach ($this->dnsServers as $dnsServer) { + $wg->add(); + + \go(function () use ($value, $dnsServer, $wg, &$failedValidator) { + try { + $validator = new BaseDNS($this->target, $this->type, $dnsServer); + $isValid = $validator->isValid($value); + + if (!$isValid) { + $failedValidator = $validator; + } + } finally { + $wg->done(); + } + }); + } + + $wg->wait(); + + if (!\is_null($failedValidator)) { + $this->count = $failedValidator->count; + $this->value = $failedValidator->value; + $this->reason = $failedValidator->reason; + $this->records = $failedValidator->records; + return false; + } + + return true; + } + + /** + * Validate with a specific DNS server + * + * @param mixed $value + * @param string $dnsServer + * @return bool + */ + protected function isValidWithDNSServer(mixed $value, string $dnsServer): bool + { + $validator = new BaseDNS($this->target, $this->type, $dnsServer); + $result = $validator->isValid($value); + + $this->count = $validator->count; + $this->value = $validator->value; + $this->reason = $validator->reason; + $this->records = $validator->records; + + return $result; + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Action.php b/src/Appwrite/Platform/Modules/Proxy/Action.php index f3a12e645c..2d365d8334 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Action.php +++ b/src/Appwrite/Platform/Modules/Proxy/Action.php @@ -3,10 +3,10 @@ namespace Appwrite\Platform\Modules\Proxy; use Appwrite\Extend\Exception; +use Appwrite\Network\Validator\DNS as ValidatorDNS; use Appwrite\Platform\Action as PlatformAction; use Utopia\Database\Document; use Utopia\DNS\Message\Record; -use Utopia\DNS\Validator\DNS as ValidatorDNS; use Utopia\Domains\Domain; use Utopia\Logger\Log; use Utopia\System\System; @@ -19,6 +19,18 @@ class Action extends PlatformAction { } + /** + * Parse DNS servers from environment variable + * + * @return array + */ + protected function getDNSServers(): array + { + $dnsEnv = System::getEnv('_APP_DNS', '8.8.8.8'); + $servers = \array_map('trim', \explode(',', $dnsEnv)); + return \array_filter($servers, fn ($server) => !empty($server)); + } + /** * Verify or re-verify a rule * @@ -31,6 +43,7 @@ class Action extends PlatformAction public function verifyRule(Document $rule, ?Log $log = null, ?string $verificationDomainAPI = null, ?string $verificationDomainFunction = null): void { $dnsValidatorClass = $this->dnsValidatorClass; + $dnsServers = $this->getDNSServers(); $domain = new Domain($rule->getAttribute('domain', '')); @@ -46,7 +59,7 @@ class Action extends PlatformAction $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); if (!empty($caaTarget)) { $validationStart = \microtime(true); - $validator = new $dnsValidatorClass($caaTarget, Record::TYPE_CAA, System::getEnv('_APP_DNS', '8.8.8.8')); + $validator = new $dnsValidatorClass($caaTarget, Record::TYPE_CAA, $dnsServers); if (!$validator->isValid($domain->get())) { if (!\is_null($log)) { $log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart)); @@ -88,7 +101,7 @@ class Action extends PlatformAction $mainValidator = null; // Validator to use for error description if (!is_null($targetCNAME)) { - $validator = new $dnsValidatorClass($targetCNAME->get(), Record::TYPE_CNAME, System::getEnv('_APP_DNS', '8.8.8.8')); + $validator = new $dnsValidatorClass($targetCNAME->get(), Record::TYPE_CNAME, $dnsServers); $validators[] = $validator; if (\is_null($mainValidator)) { @@ -98,7 +111,7 @@ class Action extends PlatformAction $targetA = System::getEnv('_APP_DOMAIN_TARGET_A', ''); if ((new IP(IP::V4))->isValid($targetA)) { - $validator = new $dnsValidatorClass($targetA, Record::TYPE_A, System::getEnv('_APP_DNS', '8.8.8.8')); + $validator = new $dnsValidatorClass($targetA, Record::TYPE_A, $dnsServers); $validators[] = $validator; if (\is_null($mainValidator)) { @@ -108,7 +121,7 @@ class Action extends PlatformAction $targetAAAA = System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''); if ((new IP(IP::V6))->isValid($targetAAAA)) { - $validator = new $dnsValidatorClass($targetAAAA, Record::TYPE_AAAA, System::getEnv('_APP_DNS', '8.8.8.8')); + $validator = new $dnsValidatorClass($targetAAAA, Record::TYPE_AAAA, $dnsServers); $validators[] = $validator; if (\is_null($mainValidator)) { diff --git a/tests/unit/Network/Validators/DNSTest.php b/tests/unit/Network/Validators/DNSTest.php new file mode 100644 index 0000000000..f00f1799de --- /dev/null +++ b/tests/unit/Network/Validators/DNSTest.php @@ -0,0 +1,53 @@ +assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals('string', $validator->getType()); + } + + public function testMultipleDNSServers(): void + { + $validator = new DNS('appwrite.io', Record::TYPE_CNAME, ['8.8.8.8', '1.1.1.1']); + + $this->assertEquals(false, $validator->isValid('')); + $this->assertEquals(false, $validator->isValid(null)); + $this->assertEquals('string', $validator->getType()); + } + + public function testValidationFailure(): void + { + $validator = new DNS('invalid-target.example.com', Record::TYPE_CNAME, ['8.8.8.8', '1.1.1.1']); + + $result = $validator->isValid('nonexistent-domain-' . \uniqid() . '.com'); + + $this->assertEquals(false, $result); + $this->assertIsInt($validator->count); + $this->assertIsString($validator->value); + $this->assertIsArray($validator->records); + $this->assertIsString($validator->getDescription()); + } + + public function testCoreDNSFailure(): void + { + // CoreDNS is configured to return cname.localhost. for stage.webapp.com + $validator = new DNS('cname.localhost.', Record::TYPE_CNAME, ['172.16.238.100', '8.8.8.8']); + + $result = $validator->isValid('stage.webapp.com'); + $this->assertEquals(false, $result); + + $result = $validator->isValid('stage-wrong-cname.webapp.com'); + $this->assertEquals(false, $result); + } +}