From 5ae148f0260aefcaf137a6ae28cf661040e54a6e Mon Sep 17 00:00:00 2001 From: Hemachandar Date: Tue, 16 Dec 2025 10:59:16 +0530 Subject: [PATCH] Add logs to rules --- app/config/collections/platform.php | 11 + app/init/constants.php | 6 + src/Appwrite/Network/Validator/DNS.php | 139 +++++-------- .../Modules/Proxy/Http/Rules/API/Create.php | 116 +++-------- .../Modules/Proxy/Http/Rules/Action.php | 188 ++++++++++++++++++ .../Proxy/Http/Rules/Function/Create.php | 116 +++-------- .../Platform/Modules/Proxy/Http/Rules/Get.php | 12 +- .../Proxy/Http/Rules/Redirect/Create.php | 116 +++-------- .../Modules/Proxy/Http/Rules/Site/Create.php | 116 +++-------- .../Proxy/Http/Rules/Verification/Update.php | 113 +++-------- .../Modules/Proxy/Http/Rules/XList.php | 12 +- src/Appwrite/Utopia/Response/Model/Rule.php | 4 +- 12 files changed, 418 insertions(+), 531 deletions(-) create mode 100644 src/Appwrite/Platform/Modules/Proxy/Http/Rules/Action.php diff --git a/app/config/collections/platform.php b/app/config/collections/platform.php index b839e51622..d44d9b725c 100644 --- a/app/config/collections/platform.php +++ b/app/config/collections/platform.php @@ -1317,6 +1317,17 @@ return [ 'array' => false, 'filters' => [], ], + [ + '$id' => ID::custom('logs'), + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 1000000, + 'signed' => true, + 'required' => false, + 'default' => '', + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ diff --git a/app/init/constants.php b/app/init/constants.php index 9c771edb0a..3168212e0d 100644 --- a/app/init/constants.php +++ b/app/init/constants.php @@ -208,6 +208,12 @@ const DELETE_TYPE_SESSION_TARGETS = 'session_targets'; const DELETE_TYPE_CSV_EXPORTS = 'csv_exports'; const DELETE_TYPE_MAINTENANCE = 'maintenance'; +// Rule statuses +const RULE_STATUS_CREATED = 'created'; // This is also the status when domain DNS verification fails. +const RULE_STATUS_CERTIFICATE_GENERATING = 'verifying'; +const RULE_STATUS_CERTIFICATE_GENERATION_FAILED = 'unverified'; +const RULE_STATUS_VERIFIED = 'verified'; + // Message types const MESSAGE_SEND_TYPE_INTERNAL = 'internal'; const MESSAGE_SEND_TYPE_EXTERNAL = 'external'; diff --git a/src/Appwrite/Network/Validator/DNS.php b/src/Appwrite/Network/Validator/DNS.php index e3c1d38015..2dc3504a96 100644 --- a/src/Appwrite/Network/Validator/DNS.php +++ b/src/Appwrite/Network/Validator/DNS.php @@ -2,115 +2,66 @@ namespace Appwrite\Network\Validator; -use Utopia\DNS\Client; -use Utopia\DNS\Message; -use Utopia\DNS\Message\Question; +use Swoole\Coroutine\WaitGroup; use Utopia\DNS\Message\Record; -use Utopia\Domains\Domain; -use Utopia\System\System; -use Utopia\Validator; +use Utopia\DNS\Validator\DNS as BaseDNS; -class DNS extends Validator +class DNS extends BaseDNS { - 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'); + 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 array $dnsServers DNS server IP(s) or domain(s) to use for validation + */ + public function __construct(string $target, int $type = Record::TYPE_CNAME, array $dnsServers = []) + { + parent::__construct($target, $type, $dnsServers[0] ?? self::DEFAULT_DNS_SERVER); + + $this->dnsServers = $dnsServers; } - public function getDescription(): string + /** + * Validate DNS record value against multiple DNS servers + * + * @param mixed $value + * @return bool + */ + public function isValid(mixed $value): bool { - return 'Invalid DNS record.'; - } + $wg = new WaitGroup(); + $failedValidator = null; - public function isValid($value): bool - { - if (!is_string($value) || trim($value) === '') { - return false; - } + foreach ($this->dnsServers as $dnsServer) { + $wg->add(); - $client = new Client($this->server); - try { - $response = $client->query(Message::query( - new Question($value, $this->type) - )); - } catch (\Throwable) { - return false; - } + \go(function () use ($value, $dnsServer, $wg, &$failedValidator) { + try { + $validator = new BaseDNS($this->target, $this->type, $dnsServer); + $isValid = $validator->isValid($value); - $typeMatches = array_filter( - $response->answers, - fn (Record $record) => $record->type === $this->type - ); - - if (empty($typeMatches)) { - if ($this->type === Record::TYPE_CAA) { - return $this->validateParentCAA($value); - } - - return false; - } - - foreach ($typeMatches as $record) { - if ($this->type === Record::TYPE_CAA) { - $valuePart = $this->extractCAAValue($record->rdata); - if ($valuePart !== '' && $valuePart === $this->target) { - return true; + if (!$isValid) { + $failedValidator = $validator; + } + } finally { + $wg->done(); } - } - - if ($record->rdata === $this->target) { - return true; - } + }); } - return false; - } + $wg->wait(); - private function validateParentCAA(string $domain): bool - { - try { - $domainInfo = new Domain($domain); - } catch (\Throwable) { + if (!\is_null($failedValidator)) { + $this->count = $failedValidator->count; + $this->value = $failedValidator->value; + $this->reason = $failedValidator->reason; + $this->records = $failedValidator->records; 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; - } - - public function getType(): string - { - return self::TYPE_STRING; + return true; } } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php index e6805f0759..fc4537301e 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/API/Create.php @@ -5,7 +5,7 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\DNS; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -14,14 +14,10 @@ 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\Logger\Log; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; -use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; -use Utopia\Validator\IP; class Create extends Action { @@ -32,8 +28,10 @@ class Create extends Action return 'createAPIRule'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/proxy/rules/api') @@ -68,108 +66,43 @@ class Create extends Action ->inject('queueForEvents') ->inject('dbForPlatform') ->inject('platform') + ->inject('log') ->callback($this->action(...)); } - public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform) + public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $platform, Log $log) { - $domains = $platform['hostnames'] ?? []; + $this->validateDomainRestrictions($domain, $platform); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $restrictions = []; - if (!empty($sitesDomain)) { - $domainLevel = \count(\explode('.', $sitesDomain)); - $restrictions[] = ValidatorDomain::createRestriction($sitesDomain, $domainLevel + 1, ['commit-', 'branch-']); - } - if (!empty($functionsDomain)) { - $domainLevel = \count(\explode('.', $functionsDomain)); - $restrictions[] = ValidatorDomain::createRestriction($functionsDomain, $domainLevel + 1); - } - $validator = new ValidatorDomain($restrictions); - - if (!$validator->isValid($domain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - $deniedDomains = [...$domains]; - - if (!empty($sitesDomain)) { - $deniedDomains[] = $sitesDomain; - } - - if (!empty($functionsDomain)) { - $deniedDomains[] = $functionsDomain; - } - - $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); - $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); - foreach ($denyListDomains as $denyListDomain) { - if (empty($denyListDomain)) { - continue; - } - $deniedDomains[] = $denyListDomain; - } - - if (\in_array($domain, $deniedDomains)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - try { - $domain = new Domain($domain); - } catch (\Throwable) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); - } - // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - - $status = 'created'; - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { - $status = 'verified'; - } - if ($status === 'created') { - $validators = []; - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); - if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) { - $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', ''), Record::TYPE_A); - } - if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA); - } - - if (empty($validators)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); - } - - $validator = new AnyOf($validators, AnyOf::TYPE_STRING); - if ($validator->isValid($domain->get())) { - $status = 'verifying'; - } - } + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); $owner = ''; if ( - ($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain)) + ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || + ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) ) { $owner = 'Appwrite'; } + $status = RULE_STATUS_CREATED; + if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) { + $status = RULE_STATUS_VERIFIED; + } + $rule = new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getSequence(), - 'domain' => $domain->get(), + 'domain' => $domain, 'status' => $status, 'type' => 'api', 'trigger' => 'manual', 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain->get()]), + 'search' => implode(' ', [$ruleId, $domain]), 'owner' => $owner, 'region' => $project->getAttribute('region') ]); @@ -180,7 +113,16 @@ class Create extends Action throw new Exception(Exception::RULE_ALREADY_EXISTS); } - if ($rule->getAttribute('status', '') === 'verifying') { + if ($rule->getAttribute('status', '') === RULE_STATUS_CREATED) { + try { + $this->verifyRule($rule, $log); + $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATING); + } catch (Exception $err) { + $rule->setAttribute('logs', $err->getMessage()); + } + } + + if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $queueForCertificates ->setDomain(new Document([ 'domain' => $rule->getAttribute('domain'), diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Action.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Action.php new file mode 100644 index 0000000000..5ec5b8b8f5 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Action.php @@ -0,0 +1,188 @@ +isValid($domain)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); + } + + $deniedDomains = [...$domains]; + + if (!empty($sitesDomain)) { + $deniedDomains[] = $sitesDomain; + } + + if (!empty($functionsDomain)) { + $deniedDomains[] = $functionsDomain; + } + + $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); + $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); + foreach ($denyListDomains as $denyListDomain) { + if (empty($denyListDomain)) { + continue; + } + $deniedDomains[] = $denyListDomain; + } + + if (\in_array($domain, $deniedDomains)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + } + + /** + * Verify or re-verify a rule + * + * @param Document $rule Rule to verify + * @param Log|null $log Log instance to add timings to + * @return void + */ + protected function verifyRule(Document $rule, ?Log $log = null): void + { + $dnsValidatorClass = $this->dnsValidatorClass; + $dnsEnv = System::getEnv('_APP_DNS', '8.8.8.8'); + $servers = \array_map('trim', \explode(',', $dnsEnv)); + $dnsServers = \array_filter($servers, fn ($server) => !empty($server)); + + $domain = new Domain($rule->getAttribute('domain', '')); + + if (empty($domain->get())) { + throw new Exception(Exception::RULE_VERIFICATION_FAILED, 'DNS verification failed as domain is not valid.'); + } + + if (!$domain->isKnown() || $domain->isTest()) { + throw new Exception(Exception::RULE_VERIFICATION_FAILED, 'DNS verification failed as domain ' . $domain->get() . ' does not resolve to a known public apex domain.'); + } + + // Ensure CAA won't block certificate issuance + $caaTarget = System::getEnv('_APP_DOMAIN_TARGET_CAA', ''); + if (!empty($caaTarget)) { + $validationStart = \microtime(true); + $validator = new $dnsValidatorClass($caaTarget, Record::TYPE_CAA, $dnsServers); + if (!$validator->isValid($domain->get())) { + if (!\is_null($log)) { + $log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart)); + $log->addTag('dnsDomain', $domain->get()); + } + throw new Exception(Exception::RULE_VERIFICATION_FAILED, $validator->getDescription()); + } + } + + $targetCNAME = null; + $ruleType = $rule->getAttribute('type', ''); + $resourceType = $rule->getAttribute('deploymentResourceType', ''); + + // Ensures different target based on rule's type, as configured by env variables + if ($resourceType === 'function') { + // For example: fra.appwrite.run + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_FUNCTIONS', '')); + } elseif ($resourceType === 'site') { + // For example: appwrite.network + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); + } elseif ($ruleType === 'api') { + // For example: fra.cloud.appwrite.io + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); + } elseif ($ruleType === 'redirect') { + // Shouldn't be needed, because redirect should always have resourceTyp too, but just in case we default to sites + // For example: appwrite.network + $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); + } + + $validators = []; + $mainValidator = null; // Validator to use for error description + + if (!is_null($targetCNAME)) { + $validator = new $dnsValidatorClass($targetCNAME->get(), Record::TYPE_CNAME, $dnsServers); + $validators[] = $validator; + + if (\is_null($mainValidator)) { + $mainValidator = $validator; + } + } + + // Ensure at least one of CNAME/A/AAAA record points to our servers properly + $targetA = System::getEnv('_APP_DOMAIN_TARGET_A', ''); + if ((new IP(IP::V4))->isValid($targetA)) { + $validator = new $dnsValidatorClass($targetA, Record::TYPE_A, $dnsServers); + $validators[] = $validator; + + if (\is_null($mainValidator)) { + $mainValidator = $validator; + } + } + + $targetAAAA = System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''); + if ((new IP(IP::V6))->isValid($targetAAAA)) { + $validator = new $dnsValidatorClass($targetAAAA, Record::TYPE_AAAA, $dnsServers); + $validators[] = $validator; + + if (\is_null($mainValidator)) { + $mainValidator = $validator; + } + } + + if (empty($validators)) { + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); + } + + $validator = new AnyOf($validators, AnyOf::TYPE_STRING); + + $validationStart = \microtime(true); + if (!$validator->isValid($domain->get())) { + if (!\is_null($log)) { + $log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart)); + $log->addTag('dnsDomain', $domain->get()); + } + throw new Exception(Exception::RULE_VERIFICATION_FAILED, $mainValidator->getDescription()); + } + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php index 575ac9b832..c88e3e5085 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Function/Create.php @@ -5,7 +5,7 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\DNS; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -15,14 +15,10 @@ 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\Logger\Log; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; -use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; -use Utopia\Validator\IP; use Utopia\Validator\Text; class Create extends Action @@ -34,8 +30,10 @@ class Create extends Action return 'createFunctionRule'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/proxy/rules/function') @@ -73,59 +71,17 @@ class Create extends Action ->inject('dbForPlatform') ->inject('dbForProject') ->inject('platform') + ->inject('log') ->callback($this->action(...)); } - public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform) + public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) { - $domains = $platform['hostnames'] ?? []; + $this->validateDomainRestrictions($domain, $platform); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $restrictions = []; - if (!empty($sitesDomain)) { - $domainLevel = \count(\explode('.', $sitesDomain)); - $restrictions[] = ValidatorDomain::createRestriction($sitesDomain, $domainLevel + 1, ['commit-', 'branch-']); - } - if (!empty($functionsDomain)) { - $domainLevel = \count(\explode('.', $functionsDomain)); - $restrictions[] = ValidatorDomain::createRestriction($functionsDomain, $domainLevel + 1); - } - $validator = new ValidatorDomain($restrictions); - - if (!$validator->isValid($domain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - $deniedDomains = [...$domains]; - - if (!empty($sitesDomain)) { - $deniedDomains[] = $sitesDomain; - } - - if (!empty($functionsDomain)) { - $deniedDomains[] = $functionsDomain; - } - - $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); - $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); - foreach ($denyListDomains as $denyListDomain) { - if (empty($denyListDomain)) { - continue; - } - $deniedDomains[] = $denyListDomain; - } - - if (\in_array($domain, $deniedDomains)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - try { - $domain = new Domain($domain); - } catch (\Throwable) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); - } - $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); @@ -134,49 +90,26 @@ class Create extends Action $deployment = $dbForProject->getDocument('deployments', $function->getAttribute('deploymentId', '')); // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - - $status = 'created'; - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { - $status = 'verified'; - } - if ($status === 'created') { - $validators = []; - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); - if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) { - $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', ''), Record::TYPE_A); - } - if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA); - } - - if (empty($validators)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); - } - - $validator = new AnyOf($validators, AnyOf::TYPE_STRING); - if ($validator->isValid($domain->get())) { - $status = 'verifying'; - } - } + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); $owner = ''; if ( - ($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain)) + ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || + ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) ) { $owner = 'Appwrite'; } + $status = RULE_STATUS_CREATED; + if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) { + $status = RULE_STATUS_VERIFIED; + } + $rule = new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getSequence(), - 'domain' => $domain->get(), + 'domain' => $domain, 'status' => $status, 'type' => 'deployment', 'trigger' => 'manual', @@ -187,7 +120,7 @@ class Create extends Action 'deploymentResourceInternalId' => $function->getSequence(), 'deploymentVcsProviderBranch' => $branch, 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain->get(), $branch]), + 'search' => implode(' ', [$ruleId, $domain, $branch]), 'owner' => $owner, 'region' => $project->getAttribute('region') ]); @@ -198,7 +131,16 @@ class Create extends Action throw new Exception(Exception::RULE_ALREADY_EXISTS); } - if ($rule->getAttribute('status', '') === 'verifying') { + if ($rule->getAttribute('status', '') === RULE_STATUS_CREATED) { + try { + $this->verifyRule($rule, $log); + $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATING); + } catch (Exception $err) { + $rule->setAttribute('logs', $err->getMessage()); + } + } + + if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $queueForCertificates ->setDomain(new Document([ 'domain' => $rule->getAttribute('domain'), diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php index 77aa3df581..4581cb3d08 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Get.php @@ -10,7 +10,6 @@ use Appwrite\Utopia\Response; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Validator\UID; -use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; class Get extends Action @@ -22,8 +21,10 @@ class Get extends Action return 'getRule'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/proxy/rules/:ruleId') @@ -65,7 +66,12 @@ class Get extends Action } $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); - $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); + + // Give priority to certificate generation logs if present + if (!empty($certificate->getAttribute('logs', ''))) { + $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); + } + $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); $response->dynamic($rule, Response::MODEL_PROXY_RULE); diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php index 50b1a4e2df..1f785f858c 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Redirect/Create.php @@ -5,7 +5,7 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\DNS; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -15,14 +15,10 @@ 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\Logger\Log; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; -use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; -use Utopia\Validator\IP; use Utopia\Validator\URL; use Utopia\Validator\WhiteList; @@ -35,8 +31,10 @@ class Create extends Action return 'createRedirectRule'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/proxy/rules/redirect') @@ -76,59 +74,17 @@ class Create extends Action ->inject('dbForPlatform') ->inject('dbForProject') ->inject('platform') + ->inject('log') ->callback($this->action(...)); } - public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform) + public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) { - $domains = $platform['hostnames'] ?? []; + $this->validateDomainRestrictions($domain, $platform); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $restrictions = []; - if (!empty($sitesDomain)) { - $domainLevel = \count(\explode('.', $sitesDomain)); - $restrictions[] = ValidatorDomain::createRestriction($sitesDomain, $domainLevel + 1, ['commit-', 'branch-']); - } - if (!empty($functionsDomain)) { - $domainLevel = \count(\explode('.', $functionsDomain)); - $restrictions[] = ValidatorDomain::createRestriction($functionsDomain, $domainLevel + 1); - } - $validator = new ValidatorDomain($restrictions); - - if (!$validator->isValid($domain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - $deniedDomains = [...$domains]; - - if (!empty($sitesDomain)) { - $deniedDomains[] = $sitesDomain; - } - - if (!empty($functionsDomain)) { - $deniedDomains[] = $functionsDomain; - } - - $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); - $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); - foreach ($denyListDomains as $denyListDomain) { - if (empty($denyListDomain)) { - continue; - } - $deniedDomains[] = $denyListDomain; - } - - if (\in_array($domain, $deniedDomains)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - try { - $domain = new Domain($domain); - } catch (\Throwable) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); - } - $collection = match ($resourceType) { 'site' => 'sites', 'function' => 'functions' @@ -139,49 +95,26 @@ class Create extends Action } // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - - $status = 'created'; - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { - $status = 'verified'; - } - if ($status === 'created') { - $validators = []; - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); - if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) { - $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', ''), Record::TYPE_A); - } - if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA); - } - - if (empty($validators)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); - } - - $validator = new AnyOf($validators, AnyOf::TYPE_STRING); - if ($validator->isValid($domain->get())) { - $status = 'verifying'; - } - } + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); $owner = ''; if ( - ($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain)) + ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || + ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) ) { $owner = 'Appwrite'; } + $status = RULE_STATUS_CREATED; + if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) { + $status = RULE_STATUS_VERIFIED; + } + $rule = new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getSequence(), - 'domain' => $domain->get(), + 'domain' => $domain, 'status' => $status, 'type' => 'redirect', 'trigger' => 'manual', @@ -191,7 +124,7 @@ class Create extends Action 'deploymentResourceId' => $resource->getId(), 'deploymentResourceInternalId' => $resource->getSequence(), 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain->get()]), + 'search' => implode(' ', [$ruleId, $domain]), 'owner' => $owner, 'region' => $project->getAttribute('region') ]); @@ -202,7 +135,16 @@ class Create extends Action throw new Exception(Exception::RULE_ALREADY_EXISTS); } - if ($rule->getAttribute('status', '') === 'verifying') { + if ($rule->getAttribute('status', '') === RULE_STATUS_CREATED) { + try { + $this->verifyRule($rule, $log); + $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATING); + } catch (Exception $err) { + $rule->setAttribute('logs', $err->getMessage()); + } + } + + if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $queueForCertificates ->setDomain(new Document([ 'domain' => $rule->getAttribute('domain'), diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php index 2071477eec..793383e3ba 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Site/Create.php @@ -5,7 +5,7 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\DNS; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; @@ -15,14 +15,10 @@ 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\Logger\Log; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; -use Utopia\Validator\AnyOf; use Utopia\Validator\Domain as ValidatorDomain; -use Utopia\Validator\IP; use Utopia\Validator\Text; class Create extends Action @@ -34,8 +30,10 @@ class Create extends Action return 'createSiteRule'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) ->setHttpPath('/v1/proxy/rules/site') @@ -73,59 +71,17 @@ class Create extends Action ->inject('dbForPlatform') ->inject('dbForProject') ->inject('platform') + ->inject('log') ->callback($this->action(...)); } - public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform) + public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $platform, Log $log) { - $domains = $platform['hostnames'] ?? []; + $this->validateDomainRestrictions($domain, $platform); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $restrictions = []; - if (!empty($sitesDomain)) { - $domainLevel = \count(\explode('.', $sitesDomain)); - $restrictions[] = ValidatorDomain::createRestriction($sitesDomain, $domainLevel + 1, ['commit-', 'branch-']); - } - if (!empty($functionsDomain)) { - $domainLevel = \count(\explode('.', $functionsDomain)); - $restrictions[] = ValidatorDomain::createRestriction($functionsDomain, $domainLevel + 1); - } - $validator = new ValidatorDomain($restrictions); - - if (!$validator->isValid($domain)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - $deniedDomains = [...$domains]; - - if (!empty($sitesDomain)) { - $deniedDomains[] = $sitesDomain; - } - - if (!empty($functionsDomain)) { - $deniedDomains[] = $functionsDomain; - } - - $denyListDomains = System::getEnv('_APP_CUSTOM_DOMAIN_DENY_LIST', ''); - $denyListDomains = \array_map('trim', explode(',', $denyListDomains)); - foreach ($denyListDomains as $denyListDomain) { - if (empty($denyListDomain)) { - continue; - } - $deniedDomains[] = $denyListDomain; - } - - if (\in_array($domain, $deniedDomains)) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.'); - } - - try { - $domain = new Domain($domain); - } catch (\Throwable) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); - } - $site = $dbForProject->getDocument('sites', $siteId); if ($site->isEmpty()) { throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); @@ -134,49 +90,26 @@ class Create extends Action $deployment = $dbForProject->getDocument('deployments', $site->getAttribute('deploymentId', '')); // TODO: (@Meldiron) Remove after 1.7.x migration - $isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5'; - $ruleId = $isMd5 ? md5($domain->get()) : ID::unique(); - - $status = 'created'; - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { - $status = 'verified'; - } - if ($status === 'created') { - $validators = []; - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); - if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) { - $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', ''), Record::TYPE_A); - } - if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA); - } - - if (empty($validators)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); - } - - $validator = new AnyOf($validators, AnyOf::TYPE_STRING); - if ($validator->isValid($domain->get())) { - $status = 'verifying'; - } - } + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); $owner = ''; if ( - ($functionsDomain != '' && \str_ends_with($domain->get(), $functionsDomain)) || - ($sitesDomain != '' && \str_ends_with($domain->get(), $sitesDomain)) + ($functionsDomain != '' && \str_ends_with($domain, $functionsDomain)) || + ($sitesDomain != '' && \str_ends_with($domain, $sitesDomain)) ) { $owner = 'Appwrite'; } + $status = RULE_STATUS_CREATED; + if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) { + $status = RULE_STATUS_VERIFIED; + } + $rule = new Document([ '$id' => $ruleId, 'projectId' => $project->getId(), 'projectInternalId' => $project->getSequence(), - 'domain' => $domain->get(), + 'domain' => $domain, 'status' => $status, 'type' => 'deployment', 'trigger' => 'manual', @@ -187,7 +120,7 @@ class Create extends Action 'deploymentResourceInternalId' => $site->getSequence(), 'deploymentVcsProviderBranch' => $branch, 'certificateId' => '', - 'search' => implode(' ', [$ruleId, $domain->get(), $branch]), + 'search' => implode(' ', [$ruleId, $domain, $branch]), 'owner' => $owner, 'region' => $project->getAttribute('region') ]); @@ -198,7 +131,16 @@ class Create extends Action throw new Exception(Exception::RULE_ALREADY_EXISTS); } - if ($rule->getAttribute('status', '') === 'verifying') { + if ($rule->getAttribute('status', '') === RULE_STATUS_CREATED) { + try { + $this->verifyRule($rule, $log); + $rule->setAttribute('status', RULE_STATUS_CERTIFICATE_GENERATING); + } catch (Exception $err) { + $rule->setAttribute('logs', $err->getMessage()); + } + } + + if ($rule->getAttribute('status', '') === RULE_STATUS_CERTIFICATE_GENERATING) { $queueForCertificates ->setDomain(new Document([ 'domain' => $rule->getAttribute('domain'), diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php index af61f25f05..844f117cd5 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Verification/Update.php @@ -5,22 +5,17 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Verification; use Appwrite\Event\Certificate; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Network\Validator\DNS; +use Appwrite\Platform\Modules\Proxy\Http\Rules\Action; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\DateTime; 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; use Utopia\Platform\Scope\HTTP; -use Utopia\System\System; -use Utopia\Validator\AnyOf; -use Utopia\Validator\IP; class Update extends Action { @@ -31,8 +26,10 @@ class Update extends Action return 'updateRuleVerification'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_PATCH) ->setHttpPath('/v1/proxy/rules/:ruleId/verification') @@ -63,6 +60,7 @@ class Update extends Action ->inject('queueForEvents') ->inject('project') ->inject('dbForPlatform') + ->inject('platform') ->inject('log') ->callback($this->action(...)); } @@ -74,6 +72,7 @@ class Update extends Action Event $queueForEvents, Document $project, Database $dbForPlatform, + array $platform, Log $log ) { $rule = $dbForPlatform->getDocument('rules', $ruleId); @@ -82,83 +81,36 @@ class Update extends Action throw new Exception(Exception::RULE_NOT_FOUND); } - $targetCNAME = null; - switch ($rule->getAttribute('type', '')) { - case 'api': - // For example: fra.cloud.appwrite.io - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')); - break; - case 'redirect': - // For example: appwrite.network - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); - break; - case 'deployment': - switch ($rule->getAttribute('deploymentResourceType', '')) { - case 'function': - // For example: fra.appwrite.run - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_FUNCTIONS', '')); - break; - case 'site': - // For example: appwrite.network - $targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_SITES', '')); - break; - default: - break; - } - // no break - default: - break; - } + $queueForEvents->setParam('ruleId', $rule->getId()); - $validators = []; - - if (!is_null($targetCNAME)) { - if ($targetCNAME->isKnown() && !$targetCNAME->isTest()) { - $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', ''), Record::TYPE_A); - } - if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) { - $validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), Record::TYPE_AAAA); - } - - if (empty($validators)) { - throw new Exception(Exception::GENERAL_SERVER_ERROR, 'At least one of domain targets environment variable must be configured.'); - } - - if ($rule->getAttribute('verification') === true) { + // If rule is already verified or in certificate generation state, don't queue for verification again + if ($rule->getAttribute('status') === RULE_STATUS_VERIFIED || $rule->getAttribute('status') === RULE_STATUS_CERTIFICATE_GENERATING) { return $response->dynamic($rule, Response::MODEL_PROXY_RULE); } - $validator = new AnyOf($validators, AnyOf::TYPE_STRING); - $domain = new Domain($rule->getAttribute('domain', '')); + try { + $this->verifyRule($rule, $log); + // Reset logs and status for the rule + $rule = $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + 'logs' => '', + 'status' => RULE_STATUS_CERTIFICATE_GENERATING, + ])); - $validationStart = \microtime(true); - if (!$validator->isValid($domain->get())) { - $log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart)); - $log->addTag('dnsDomain', $domain->get()); - 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', ''), Record::TYPE_CAA); - if (!$validator->isValid($domain->get())) { - $log->addExtra('dnsTimingCaa', \strval(\microtime(true) - $validationStart)); - $log->addTag('dnsDomain', $domain->get()); - $error = $validator->getDescription(); - $log->addExtra('dnsResponse', \is_array($error) ? \json_encode($error) : \strval($error)); - throw new Exception(Exception::RULE_VERIFICATION_FAILED, 'Domain verification failed because CAA records do not allow Appwrite\'s certificate issuer.'); + $certificateId = $rule->getAttribute('certificateId', ''); + // Reset logs for the associated certificate. + if (!empty($certificateId)) { + $certificate = $dbForPlatform->updateDocument('certificates', $certificateId, new Document([ + 'logs' => '', + ])); } + } catch (Exception $err) { + $dbForPlatform->updateDocument('rules', $rule->getId(), new Document([ + '$updatedAt' => DateTime::now(), + ])); + throw $err; } - $dbForPlatform->updateDocument('rules', $rule->getId(), $rule->setAttribute('status', 'verifying')); - - // Issue a TLS certificate when domain is verified + // Issue a TLS certificate when DNS verification is successful $queueForCertificates ->setDomain(new Document([ 'domain' => $rule->getAttribute('domain'), @@ -166,10 +118,9 @@ class Update extends Action ])) ->trigger(); - $queueForEvents->setParam('ruleId', $rule->getId()); - - $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); - $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); + if (!empty($certificate)) { + $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); + } $response->dynamic($rule, Response::MODEL_PROXY_RULE); } diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php index 86f63bc258..198bf55a6f 100644 --- a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/XList.php @@ -13,7 +13,6 @@ use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; -use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\Validator\Boolean; use Utopia\Validator\Text; @@ -27,8 +26,10 @@ class XList extends Action return 'listRules'; } - public function __construct() + public function __construct(...$params) { + parent::__construct(...$params); + $this ->setHttpMethod(Action::HTTP_REQUEST_METHOD_GET) ->setHttpPath('/v1/proxy/rules') @@ -109,7 +110,12 @@ class XList extends Action $rules = $dbForPlatform->find('rules', $queries); foreach ($rules as $rule) { $certificate = $dbForPlatform->getDocument('certificates', $rule->getAttribute('certificateId', '')); - $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); + + // Give priority to certificate generation logs if present + if (!empty($certificate->getAttribute('logs', ''))) { + $rule->setAttribute('logs', $certificate->getAttribute('logs', '')); + } + $rule->setAttribute('renewAt', $certificate->getAttribute('renewDate', '')); } diff --git a/src/Appwrite/Utopia/Response/Model/Rule.php b/src/Appwrite/Utopia/Response/Model/Rule.php index d4b8ffd9e7..86ac6f470e 100644 --- a/src/Appwrite/Utopia/Response/Model/Rule.php +++ b/src/Appwrite/Utopia/Response/Model/Rule.php @@ -92,9 +92,9 @@ class Rule extends Model ]) ->addRule('logs', [ 'type' => self::TYPE_STRING, - 'description' => 'Certificate generation logs. This will return an empty string if generation did not run, or succeeded.', + 'description' => 'Logs from rule verification or certificate generation. Certificate generation logs are prioritized if both are available.', 'default' => '', - 'example' => 'HTTP challegne failed.', + 'example' => 'Verification of DNS records failed with DNS resolver 8.8.8.8. Domain stage.myapp.com does not have DNS record.', ]) ->addRule('renewAt', [ 'type' => self::TYPE_DATETIME,