Merge remote-tracking branch 'origin/1.8.x' into feat-appwrite-auth

This commit is contained in:
Damodar Lohani
2025-11-05 01:59:42 +00:00
19 changed files with 137 additions and 198 deletions
+63 -95
View File
@@ -3,118 +3,65 @@
namespace Appwrite\Network\Validator;
use Utopia\DNS\Client;
use Utopia\DNS\Message;
use Utopia\DNS\Message\Question;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\System\System;
use Utopia\Validator;
class DNS extends Validator
{
public const RECORD_A = 'A';
public const RECORD_AAAA = 'AAAA';
public const RECORD_CNAME = 'CNAME';
public const RECORD_CAA = 'CAA'; // You can provide domain only (as $target) for CAA validation
/**
* @var mixed
*/
protected mixed $logs;
/**
* @var string
*/
protected string $dnsServer;
/**
* @param string $target
*/
public function __construct(protected string $target, protected string $type = self::RECORD_CNAME, string $dnsServer = '')
{
if (empty($dnsServer)) {
$dnsServer = System::getEnv('_APP_DNS', '8.8.8.8');
}
$this->dnsServer = $dnsServer;
public function __construct(
protected string $target,
protected int $type = Record::TYPE_CNAME,
protected string $server = ''
) {
$this->server = $server ?: System::getEnv('_APP_DNS', '8.8.8.8');
}
/**
* @return string
*/
public function getDescription(): string
{
return 'Invalid DNS record';
return 'Invalid DNS record.';
}
/**
* @return mixed
*/
public function getLogs(): mixed
{
return $this->logs;
}
/**
* Check if DNS record value matches specific value
*
* @param mixed $domain
* @return bool
*/
public function isValid($value): bool
{
if (!is_string($value)) {
if (!is_string($value) || trim($value) === '') {
return false;
}
$dns = new Client($this->dnsServer);
$client = new Client($this->server);
try {
$rawQuery = $dns->query($value, $this->type);
// Some DNS servers return all records, not only type that's asked for
// Likely occurs when no records of specific type are found
$query = array_filter($rawQuery, function ($record) {
return $record->getTypeName() === $this->type;
});
$this->logs = $query;
} catch (\Exception $e) {
$this->logs = ['error' => $e->getMessage()];
$response = $client->query(Message::query(
new Question($value, $this->type)
));
} catch (\Throwable) {
return false;
}
if (empty($query)) {
// CAA records inherit from parent (custom CAA behaviour)
if ($this->type === self::RECORD_CAA) {
$domain = new Domain($value);
if ($domain->get() === $domain->getApex()) {
return true; // No CAA on apex domain means anyone can issue certificate
}
$typeMatches = array_filter(
$response->answers,
fn (Record $record) => $record->type === $this->type
);
// Recursive validation by parent domain
$parts = \explode('.', $value);
\array_shift($parts);
$parentDomain = \implode('.', $parts);
$validator = new DNS($this->target, DNS::RECORD_CAA, $this->dnsServer);
return $validator->isValid($parentDomain);
if (empty($typeMatches)) {
if ($this->type === Record::TYPE_CAA) {
return $this->validateParentCAA($value);
}
return false;
}
foreach ($query as $record) {
// CAA validation only needs to ensure domain
if ($this->type === self::RECORD_CAA) {
// Extract domain; comments showcase extraction steps in most complex scenario
$rdata = $record->getRdata(); // 255 issuewild "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"
$rdata = \explode(' ', $rdata, 3)[2] ?? ''; // "certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600"
$rdata = \trim($rdata, '"'); // certainly.com;validationmethods=tls-alpn-01;retrytimeout=3600
$rdata = \explode(';', $rdata, 2)[0] ?? ''; // certainly.com
if ($rdata === $this->target) {
foreach ($typeMatches as $record) {
if ($this->type === Record::TYPE_CAA) {
$valuePart = $this->extractCAAValue($record->rdata);
if ($valuePart !== '' && $valuePart === $this->target) {
return true;
}
}
if ($record->getRdata() === $this->target) {
if ($record->rdata === $this->target) {
return true;
}
}
@@ -122,25 +69,46 @@ class DNS extends Validator
return false;
}
/**
* Is array
*
* Function will return true if object is array.
*
* @return bool
*/
private function validateParentCAA(string $domain): bool
{
try {
$domainInfo = new Domain($domain);
} catch (\Throwable) {
return false;
}
if ($domainInfo->get() === $domainInfo->getApex()) {
return true;
}
$parts = explode('.', $domainInfo->get());
array_shift($parts);
$parent = implode('.', $parts);
if ($parent === '') {
return false;
}
$validator = new self($this->target, Record::TYPE_CAA, $this->server);
return $validator->isValid($parent);
}
private function extractCAAValue(string $rdata): string
{
$parts = explode(' ', $rdata, 3);
if (count($parts) < 3) {
return '';
}
$value = trim($parts[2], '"');
return explode(';', $value)[0] ?? '';
}
public function isArray(): bool
{
return false;
}
/**
* Get Type
*
* Returns validator type.
*
* @return string
*/
public function getType(): string
{
return self::TYPE_STRING;
@@ -14,6 +14,7 @@ use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -135,13 +136,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -147,13 +148,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -152,13 +153,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -15,6 +15,7 @@ use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Platform\Action;
use Utopia\Platform\Scope\HTTP;
@@ -147,13 +148,13 @@ class Create extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -13,6 +13,7 @@ use Appwrite\Utopia\Response;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Validator\UID;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Logger\Log;
use Utopia\Platform\Action;
@@ -113,15 +114,15 @@ class Update extends Action
if (!is_null($targetCNAME)) {
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
if (empty($validators)) {
@@ -139,24 +140,13 @@ class Update extends Action
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$errors = [];
foreach ($validators as $validator) {
if (!empty($validator->getLogs())) {
$errors[] = $validator->getLogs();
}
}
$error = \implode("\n", $errors);
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
throw new Exception(Exception::RULE_VERIFICATION_FAILED);
}
// Ensure CAA won't block certificate issuance
if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) {
$validationStart = \microtime(true);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), Record::TYPE_CAA);
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
@@ -57,7 +57,7 @@ class Create extends Action
group: 'deployments',
name: 'createDeployment',
description: <<<EOT
Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the function's deployment to use your new deployment ID.
Create a new site code deployment. Use this endpoint to upload a new version of your site code. To activate your newly uploaded code, you'll need to update the site's deployment to use your new deployment ID.
EOT,
auth: [AuthType::KEY],
responses: [
+5 -15
View File
@@ -22,6 +22,7 @@ use Utopia\Database\Exception\Conflict;
use Utopia\Database\Exception\Structure;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\DNS\Message\Record;
use Utopia\Domains\Domain;
use Utopia\Locale\Locale;
use Utopia\Logger\Log;
@@ -313,13 +314,13 @@ class Certificates extends Action
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_CNAME);
$validators[] = new DNS($targetCNAME->get(), Record::TYPE_CNAME);
}
if ((new IP(IP::V4))->isValid(System::getEnv('_APP_DOMAIN_TARGET_A', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), DNS::RECORD_A);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_A', ''), Record::TYPE_A);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA);
}
// Validate if domain target is properly configured
@@ -332,24 +333,13 @@ class Certificates extends Action
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$errors = [];
foreach ($validators as $validator) {
if (!empty($validator->getLogs())) {
$errors[] = $validator->getLogs();
}
}
$error = \implode("\n", $errors);
$log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error));
throw new Exception('Failed to verify domain DNS records.');
}
// Ensure CAA won't block certificate issuance
if (!empty(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''))) {
$validationStart = \microtime(true);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), DNS::RECORD_CAA);
$validator = new DNS(System::getEnv('_APP_DOMAIN_TARGET_CAA', ''), Record::TYPE_CAA);
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());