Merge pull request #9627 from appwrite/feat-a-record

Feat: A/AAA record support
This commit is contained in:
Matej Bačo
2025-04-09 11:13:28 +02:00
committed by GitHub
18 changed files with 343 additions and 156 deletions
+3 -1
View File
@@ -23,7 +23,9 @@ _APP_OPENSSL_KEY_V1=your-secret-key
_APP_DOMAIN=traefik
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
_APP_DOMAIN_TARGET=test.appwrite.io
_APP_DOMAIN_TARGET_CNAME=test.appwrite.io
_APP_DOMAIN_TARGET_A=127.0.0.1
_APP_DOMAIN_TARGET_AAAA=::1
_APP_RULES_FORMAT=md5
_APP_REDIS_HOST=redis
_APP_REDIS_PORT=6379
+28 -1
View File
@@ -90,13 +90,40 @@ return [
],
[
'name' => '_APP_DOMAIN_TARGET',
'description' => 'A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite \'_APP_DOMAIN\' variable. The default value is \'localhost\'.',
'description' => 'Deprecated since 1.7.0. A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite \'_APP_DOMAIN\' variable. The default value is \'localhost\'.',
'introduction' => '',
'default' => 'localhost',
'required' => true,
'question' => 'Enter a DNS A record hostname to serve as a CNAME for your custom domains.' . PHP_EOL . 'You can use the same value as used for the Appwrite hostname.',
'filter' => 'domainTarget'
],
[
'name' => '_APP_DOMAIN_TARGET_CNAME',
'description' => 'A domain that can be used as DNS CNAME record to point to instance of Appwrite server.',
'introduction' => '',
'default' => 'localhost',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_DOMAIN_TARGET_AAAA',
'description' => 'An IPv6 that can be used as DNS AAAA record to point to instance of Appwrite server.',
'introduction' => '',
'default' => '::1',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_DOMAIN_TARGET_A',
'description' => 'An IPV4 that can be used as DNS A record to point to instance of Appwrite server.',
'introduction' => '',
'default' => '127.0.0.1',
'required' => false,
'question' => '',
'filter' => ''
],
[
'name' => '_APP_CONSOLE_WHITELIST_ROOT',
'description' => 'This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled.',
+20 -5
View File
@@ -8,7 +8,9 @@ use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Document;
use Utopia\Domains\Domain;
use Utopia\System\System;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
App::init()
@@ -40,10 +42,21 @@ App::get('/v1/console/variables')
))
->inject('response')
->action(function (Response $response) {
$isDomainEnabled = !empty(System::getEnv('_APP_DOMAIN', ''))
&& !empty(System::getEnv('_APP_DOMAIN_TARGET', ''))
&& System::getEnv('_APP_DOMAIN', '') !== 'localhost'
&& System::getEnv('_APP_DOMAIN_TARGET', '') !== 'localhost';
$validator = new Domain(System::getEnv('_APP_DOMAIN'));
$isDomainValid = !empty(System::getEnv('_APP_DOMAIN', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
$isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new IP(IP::V4);
$isAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_A', '')) && ($validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_A')));
$validator = new IP(IP::V6);
$isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'));
$isDomainEnabled = $isDomainValid && (
$isAAAAValid || $isAValid || $isCNAMEValid
);
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))
@@ -54,7 +67,9 @@ App::get('/v1/console/variables')
$isAssistantEnabled = !empty(System::getEnv('_APP_ASSISTANT_OPENAI_API_KEY', ''));
$variables = new Document([
'_APP_DOMAIN_TARGET' => System::getEnv('_APP_DOMAIN_TARGET'),
'_APP_DOMAIN_TARGET_CNAME' => System::getEnv('_APP_DOMAIN_TARGET_CNAME'),
'_APP_DOMAIN_TARGET_AAAA' => System::getEnv('_APP_DOMAIN_TARGET_AAAA'),
'_APP_DOMAIN_TARGET_A' => System::getEnv('_APP_DOMAIN_TARGET_A'),
'_APP_STORAGE_LIMIT' => +System::getEnv('_APP_STORAGE_LIMIT'),
'_APP_COMPUTE_SIZE_LIMIT' => +System::getEnv('_APP_COMPUTE_SIZE_LIMIT'),
'_APP_USAGE_STATS' => System::getEnv('_APP_USAGE_STATS'),
+25 -6
View File
@@ -4,7 +4,7 @@ use Appwrite\Event\Certificate;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@@ -21,6 +21,8 @@ use Utopia\Database\Validator\UID;
use Utopia\Domains\Domain;
use Utopia\Logger\Log;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\IP;
use Utopia\Validator\Text;
App::get('/v1/proxy/rules')
@@ -208,17 +210,27 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
throw new Exception(Exception::RULE_NOT_FOUND);
}
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_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);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
}
if (!$target->isKnown() || $target->isTest()) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Domain target must be configured as environment variable.');
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) {
return $response->dynamic($rule, Response::MODEL_PROXY_RULE);
}
$validator = new CNAME($target->get()); // Verify Domain with DNS records
$validator = new AnyOf($validators, AnyOf::TYPE_STRING);
$domain = new Domain($rule->getAttribute('domain', ''));
$validationStart = \microtime(true);
@@ -226,7 +238,14 @@ App::patch('/v1/proxy/rules/:ruleId/verification')
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$error = $validator->getLogs();
$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);
Generated
+24 -24
View File
@@ -1365,16 +1365,16 @@
},
{
"name": "open-telemetry/sdk",
"version": "1.2.2",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/opentelemetry-php/sdk.git",
"reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0"
"reference": "0e7804c176c4b09d95b7985400aa38ce544cb7fc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/37eec0fe47ddd627911f318f29b6cd48196be0c0",
"reference": "37eec0fe47ddd627911f318f29b6cd48196be0c0",
"url": "https://api.github.com/repos/opentelemetry-php/sdk/zipball/0e7804c176c4b09d95b7985400aa38ce544cb7fc",
"reference": "0e7804c176c4b09d95b7985400aa38ce544cb7fc",
"shasum": ""
},
"require": {
@@ -1451,7 +1451,7 @@
"issues": "https://github.com/open-telemetry/opentelemetry-php/issues",
"source": "https://github.com/open-telemetry/opentelemetry-php"
},
"time": "2025-01-29T21:40:28+00:00"
"time": "2025-04-08T09:55:41+00:00"
},
{
"name": "open-telemetry/sem-conv",
@@ -3791,16 +3791,16 @@
},
{
"name": "utopia-php/image",
"version": "0.8.1",
"version": "0.8.2",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/image.git",
"reference": "e8cc7dd14f423270a1b7570ec0dae88a66195b63"
"reference": "6c736965177f9a9e71311e22b80cfa88511768e9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/image/zipball/e8cc7dd14f423270a1b7570ec0dae88a66195b63",
"reference": "e8cc7dd14f423270a1b7570ec0dae88a66195b63",
"url": "https://api.github.com/repos/utopia-php/image/zipball/6c736965177f9a9e71311e22b80cfa88511768e9",
"reference": "6c736965177f9a9e71311e22b80cfa88511768e9",
"shasum": ""
},
"require": {
@@ -3834,9 +3834,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/image/issues",
"source": "https://github.com/utopia-php/image/tree/0.8.1"
"source": "https://github.com/utopia-php/image/tree/0.8.2"
},
"time": "2025-04-04T18:55:20+00:00"
"time": "2025-04-08T11:31:45+00:00"
},
{
"name": "utopia-php/locale",
@@ -3996,16 +3996,16 @@
},
{
"name": "utopia-php/migration",
"version": "0.8.4",
"version": "0.8.5",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/migration.git",
"reference": "845fd04ccf5e0edb03c184b864e0596080a432b8"
"reference": "0dd95b148c581579ec05d2abbbdc13c2b4702331"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/845fd04ccf5e0edb03c184b864e0596080a432b8",
"reference": "845fd04ccf5e0edb03c184b864e0596080a432b8",
"url": "https://api.github.com/repos/utopia-php/migration/zipball/0dd95b148c581579ec05d2abbbdc13c2b4702331",
"reference": "0dd95b148c581579ec05d2abbbdc13c2b4702331",
"shasum": ""
},
"require": {
@@ -4046,9 +4046,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/migration/issues",
"source": "https://github.com/utopia-php/migration/tree/0.8.4"
"source": "https://github.com/utopia-php/migration/tree/0.8.5"
},
"time": "2025-03-28T02:08:22+00:00"
"time": "2025-04-09T05:21:09+00:00"
},
{
"name": "utopia-php/orchestration",
@@ -5085,16 +5085,16 @@
},
{
"name": "laravel/pint",
"version": "v1.21.2",
"version": "v1.22.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/pint.git",
"reference": "370772e7d9e9da087678a0edf2b11b6960e40558"
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/pint/zipball/370772e7d9e9da087678a0edf2b11b6960e40558",
"reference": "370772e7d9e9da087678a0edf2b11b6960e40558",
"url": "https://api.github.com/repos/laravel/pint/zipball/7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"reference": "7ddfaa6523a675fae5c4123ee38fc6bfb8ee4f36",
"shasum": ""
},
"require": {
@@ -5105,9 +5105,9 @@
"php": "^8.2.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.72.0",
"friendsofphp/php-cs-fixer": "^3.75.0",
"illuminate/view": "^11.44.2",
"larastan/larastan": "^3.2.0",
"larastan/larastan": "^3.3.1",
"laravel-zero/framework": "^11.36.1",
"mockery/mockery": "^1.6.12",
"nunomaduro/termwind": "^2.3",
@@ -5147,7 +5147,7 @@
"issues": "https://github.com/laravel/pint/issues",
"source": "https://github.com/laravel/pint"
},
"time": "2025-03-14T22:31:42+00:00"
"time": "2025-04-08T22:11:45+00:00"
},
{
"name": "matthiasmullie/minify",
+12 -4
View File
@@ -115,7 +115,9 @@ services:
- _APP_OPTIONS_COMPUTE_FORCE_HTTPS
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_TARGET_CNAME
- _APP_DOMAIN_TARGET_AAAA
- _APP_DOMAIN_TARGET_A
- _APP_DOMAIN_FUNCTIONS
- _APP_REDIS_HOST
- _APP_REDIS_PORT
@@ -524,7 +526,9 @@ services:
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_TARGET_CNAME
- _APP_DOMAIN_TARGET_AAAA
- _APP_DOMAIN_TARGET_A
- _APP_DOMAIN_FUNCTIONS
- _APP_EMAIL_CERTIFICATES
- _APP_REDIS_HOST
@@ -690,7 +694,9 @@ services:
- _APP_WORKER_PER_CORE
- _APP_OPENSSL_KEY_V1
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_TARGET_CNAME
- _APP_DOMAIN_TARGET_AAAA
- _APP_DOMAIN_TARGET_A
- _APP_EMAIL_SECURITY
- _APP_REDIS_HOST
- _APP_REDIS_PORT
@@ -722,7 +728,9 @@ services:
- _APP_ENV
- _APP_WORKER_PER_CORE
- _APP_DOMAIN
- _APP_DOMAIN_TARGET
- _APP_DOMAIN_TARGET_CNAME
- _APP_DOMAIN_TARGET_AAAA
- _APP_DOMAIN_TARGET_A
- _APP_DOMAIN_FUNCTIONS
- _APP_OPENSSL_KEY_V1
- _APP_REDIS_HOST
@@ -4,24 +4,22 @@ namespace Appwrite\Network\Validator;
use Utopia\Validator;
class CNAME extends Validator
class DNS extends Validator
{
public const RECORD_A = 'a';
public const RECORD_AAAA = 'aaaa';
public const RECORD_CNAME = 'cname';
/**
* @var mixed
*/
protected mixed $logs;
/**
* @var string
*/
protected $target;
/**
* @param string $target
*/
public function __construct($target)
public function __construct(protected $target, protected string $type = self::RECORD_CNAME)
{
$this->target = $target;
}
/**
@@ -29,7 +27,7 @@ class CNAME extends Validator
*/
public function getDescription(): string
{
return 'Invalid CNAME record';
return 'Invalid DNS record';
}
/**
@@ -41,20 +39,34 @@ class CNAME extends Validator
}
/**
* Check if CNAME record target value matches selected target
* Check if DNS record value matches specific value
*
* @param mixed $domain
*
* @return bool
*/
public function isValid($domain): bool
public function isValid($value): bool
{
if (!is_string($domain)) {
$typeNative = match ($this->type) {
self::RECORD_A => DNS_A,
self::RECORD_AAAA => DNS_AAAA,
self::RECORD_CNAME => DNS_CNAME,
default => throw new \Exception('Record type not supported.')
};
$dnsKey = match ($this->type) {
self::RECORD_A => 'ip',
self::RECORD_AAAA => 'ipv6',
self::RECORD_CNAME => 'target',
default => throw new \Exception('Record type not supported.')
};
if (!is_string($value)) {
return false;
}
try {
$records = \dns_get_record($domain, DNS_CNAME);
$records = \dns_get_record($value, $typeNative);
$this->logs = $records;
} catch (\Throwable $th) {
return false;
@@ -65,7 +77,7 @@ class CNAME extends Validator
}
foreach ($records as $record) {
if (isset($record['target']) && $record['target'] === $this->target) {
if (isset($record[$dnsKey]) && $record[$dnsKey] === $this->target) {
return true;
}
}
@@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\API;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
@@ -19,7 +18,9 @@ use Utopia\Domains\Domain;
use Utopia\Platform\Action;
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
{
@@ -95,13 +96,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
@@ -110,8 +104,23 @@ class Create extends Action
$status = 'verified';
}
if ($status === 'created') {
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($target->get());
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_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);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_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';
}
@@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Function;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
@@ -20,7 +19,9 @@ use Utopia\Domains\Domain;
use Utopia\Platform\Action;
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
@@ -96,13 +97,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
$function = $dbForProject->getDocument('functions', $functionId);
if ($function->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
@@ -118,8 +112,23 @@ class Create extends Action
$status = 'verified';
}
if ($status === 'created') {
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($target->get());
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_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);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_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';
}
@@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Redirect;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
@@ -19,7 +18,9 @@ use Utopia\Domains\Domain;
use Utopia\Platform\Action;
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;
@@ -95,13 +96,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
@@ -110,8 +104,23 @@ class Create extends Action
$status = 'verified';
}
if ($status === 'created') {
$dnsTarget = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($dnsTarget->get());
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_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);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_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';
}
@@ -5,12 +5,11 @@ namespace Appwrite\Platform\Modules\Proxy\Http\Rules\Site;
use Appwrite\Event\Certificate;
use Appwrite\Event\Event;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\DNS;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
use Appwrite\Utopia\Response;
use Utopia\App;
use Utopia\Database\Database;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
@@ -20,7 +19,9 @@ use Utopia\Domains\Domain;
use Utopia\Platform\Action;
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
@@ -96,13 +97,6 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.');
}
// Apex domain prevention due to CNAME limitations
if (empty(App::getEnv('_APP_DOMAINS_NAMESERVERS', ''))) {
if ($domain->get() === $domain->getRegisterable()) {
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'The instance does not allow root-level (apex) domains.');
}
}
$site = $dbForProject->getDocument('sites', $siteId);
if ($site->isEmpty()) {
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
@@ -118,8 +112,23 @@ class Create extends Action
$status = 'verified';
}
if ($status === 'created') {
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validator = new CNAME($target->get());
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_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);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_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';
}
+20 -7
View File
@@ -15,6 +15,7 @@ use Utopia\Registry\Registry;
use Utopia\Storage\Device\Local;
use Utopia\Storage\Storage;
use Utopia\System\System;
use Utopia\Validator\IP;
class Doctor extends Action
{
@@ -43,19 +44,31 @@ class Doctor extends Action
Console::log('[Settings]');
$domain = new Domain(System::getEnv('_APP_DOMAIN'));
if (!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 Hostname has no public suffix (' . $domain->get() . ')');
Console::log('🔴 Hostname is not valid (' . $domain->get() . ')');
} else {
Console::log('🟢 Hostname has a public suffix (' . $domain->get() . ')');
Console::log('🟢 Hostname is valid (' . $domain->get() . ')');
}
$domain = new Domain(System::getEnv('_APP_DOMAIN_TARGET'));
$domain = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
if (!$domain->isKnown() || $domain->isTest()) {
Console::log('🔴 CNAME target has no public suffix (' . $domain->get() . ')');
Console::log('🔴 CNAME record target is not valid (' . $domain->get() . ')');
} else {
Console::log('🟢 CNAME target has a public suffix (' . $domain->get() . ')');
Console::log('🟢 CNAME record target is valid (' . $domain->get() . ')');
}
$ipv4 = new IP(IP::V4);
if (!$ipv4->isValid(System::getEnv('_APP_DOMAIN_TARGET_A'))) {
Console::log('🔴 A record target is not valid (' . System::getEnv('_APP_DOMAIN_TARGET_A') . ')');
} else {
Console::log('🟢 A record target is valid (' . System::getEnv('_APP_DOMAIN_TARGET_A') . ')');
}
$ipv6 = new IP(IP::V6);
if (!$ipv6->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'))) {
Console::log('🔴 AAAA record target is not valid (' . System::getEnv('_APP_DOMAIN_TARGET_AAAA') . ')');
} else {
Console::log('🟢 AAAA record target is valid (' . System::getEnv('_APP_DOMAIN_TARGET_AAAA') . ')');
}
if (System::getEnv('_APP_OPENSSL_KEY_V1') === 'your-secret-key' || empty(System::getEnv('_APP_OPENSSL_KEY_V1'))) {
+28 -9
View File
@@ -8,7 +8,7 @@ use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Realtime;
use Appwrite\Event\Webhook;
use Appwrite\Network\Validator\CNAME;
use Appwrite\Network\Validator\DNS;
use Appwrite\Template\Template;
use Appwrite\Utopia\Response\Model\Rule;
use Exception;
@@ -28,6 +28,8 @@ use Utopia\Logger\Log;
use Utopia\Platform\Action;
use Utopia\Queue\Message;
use Utopia\System\System;
use Utopia\Validator\AnyOf;
use Utopia\Validator\IP;
class Certificates extends Action
{
@@ -290,22 +292,39 @@ class Certificates extends Action
}
if (!$isMainDomain) {
// TODO: Would be awesome to also support A/AAAA records here. Maybe dry run?
// Validate if domain target is properly configured
$target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', ''));
$validationStart = \microtime(true);
if (!$target->isKnown() || $target->isTest()) {
throw new Exception('Unreachable CNAME target (' . $target->get() . '), please use a domain with a public suffix.');
$validators = [];
$targetCNAME = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME', ''));
if (!$targetCNAME->isKnown() || $targetCNAME->isTest()) {
$validators[] = new DNS($targetCNAME->get(), DNS::RECORD_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);
}
if ((new IP(IP::V6))->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''))) {
$validators[] = new DNS(System::getEnv('_APP_DOMAIN_TARGET_AAAA', ''), DNS::RECORD_AAAA);
}
// Validate if domain target is properly configured
if (empty($validators)) {
throw new Exception('At least one of domain targets environment variable must be configured.');
}
// Verify domain with DNS records
$validationStart = \microtime(true);
$validator = new CNAME($target->get());
$validator = new AnyOf($validators, AnyOf::TYPE_STRING);
if (!$validator->isValid($domain->get())) {
$log->addExtra('dnsTiming', \strval(\microtime(true) - $validationStart));
$log->addTag('dnsDomain', $domain->get());
$error = $validator->getLogs();
$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.');
@@ -10,12 +10,24 @@ class ConsoleVariables extends Model
public function __construct()
{
$this
->addRule('_APP_DOMAIN_TARGET', [
'type' => self::TYPE_STRING,
'description' => 'CNAME target for your Appwrite custom domains.',
'default' => '',
'example' => 'appwrite.io',
])
->addRule('_APP_DOMAIN_TARGET_CNAME', [
'type' => self::TYPE_STRING,
'description' => 'CNAME target for your Appwrite custom domains.',
'default' => '',
'example' => 'appwrite.io',
])
->addRule('_APP_DOMAIN_TARGET_A', [
'type' => self::TYPE_STRING,
'description' => 'A target for your Appwrite custom domains.',
'default' => '',
'example' => '127.0.0.1',
])
->addRule('_APP_DOMAIN_TARGET_AAAA', [
'type' => self::TYPE_STRING,
'description' => 'AAAA target for your Appwrite custom domains.',
'default' => '',
'example' => '::1',
])
->addRule('_APP_STORAGE_LIMIT', [
'type' => self::TYPE_INTEGER,
'description' => 'Maximum file size allowed for file upload in bytes.',
@@ -24,11 +24,12 @@ class ConsoleConsoleClientTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertCount(11, $response['body']);
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET']);
$this->assertCount(13, $response['body']);
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_CNAME']);
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_A']);
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET_AAAA']);
$this->assertIsInt($response['body']['_APP_STORAGE_LIMIT']);
$this->assertIsInt($response['body']['_APP_COMPUTE_SIZE_LIMIT']);
$this->assertIsString($response['body']['_APP_DOMAIN_TARGET']);
$this->assertIsBool($response['body']['_APP_DOMAIN_ENABLED']);
$this->assertIsBool($response['body']['_APP_VCS_ENABLED']);
$this->assertIsBool($response['body']['_APP_ASSISTANT_ENABLED']);
@@ -53,8 +53,10 @@ class ProxyCustomServerTest extends Scope
public function testCreateRuleApex(): void
{
$rule = $this->createAPIRule('myapp.com');
$this->assertEquals(400, $rule['headers']['status-code']);
$domain = \uniqid() . '.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
$this->assertEquals('created', $rule['body']['status']);
}
public function testCreateRuleVcs(): void
@@ -337,7 +339,7 @@ class ProxyCustomServerTest extends Scope
$this->cleanupRule($rule['body']['$id']);
// Create + update
$domain = \uniqid() . '-cname-api.custom.localhost';
$domain = \uniqid() . '-cname-api.custom.com';
$rule = $this->createAPIRule($domain);
$this->assertEquals(201, $rule['headers']['status-code']);
@@ -1,29 +0,0 @@
<?php
namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Validator\CNAME;
use PHPUnit\Framework\TestCase;
class CNAMETest extends TestCase
{
protected ?CNAME $object = null;
public function setUp(): void
{
$this->object = new CNAME('appwrite.io');
}
public function tearDown(): void
{
}
public function testValues(): void
{
$this->assertEquals($this->object->isValid(''), false);
$this->assertEquals($this->object->isValid(null), false);
$this->assertEquals($this->object->isValid(false), false);
$this->assertEquals($this->object->isValid('cname-unit-test.appwrite.org'), true);
$this->assertEquals($this->object->isValid('test1.appwrite.org'), false);
}
}
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Validator\DNS;
use PHPUnit\Framework\TestCase;
class DNSTest extends TestCase
{
public function setUp(): void
{
}
public function tearDown(): void
{
}
public function testCNAME(): void
{
$validator = new DNS('appwrite.io', DNS::RECORD_CNAME);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
$this->assertEquals($validator->isValid('cname-unit-test.appwrite.org'), true);
$this->assertEquals($validator->isValid('test1.appwrite.org'), false);
}
public function testA(): void
{
// IPv4 for documentation purposes
$validator = new DNS('203.0.113.1', DNS::RECORD_A);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
$this->assertEquals($validator->isValid('a-unit-test.appwrite.org'), true);
$this->assertEquals($validator->isValid('test1.appwrite.org'), false);
}
public function testAAAA(): void
{
// IPv6 for documentation purposes
$validator = new DNS('2001:db8::1', DNS::RECORD_AAAA);
$this->assertEquals($validator->isValid(''), false);
$this->assertEquals($validator->isValid(null), false);
$this->assertEquals($validator->isValid(false), false);
$this->assertEquals($validator->isValid('aaaa-unit-test.appwrite.org'), true);
$this->assertEquals($validator->isValid('test1.appwrite.org'), false);
}
}