mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Add logs to rules
This commit is contained in:
@@ -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' => [
|
||||
[
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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<string> $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);
|
||||
\go(function () use ($value, $dnsServer, $wg, &$failedValidator) {
|
||||
try {
|
||||
$response = $client->query(Message::query(
|
||||
new Question($value, $this->type)
|
||||
));
|
||||
} catch (\Throwable) {
|
||||
$validator = new BaseDNS($this->target, $this->type, $dnsServer);
|
||||
$isValid = $validator->isValid($value);
|
||||
|
||||
if (!$isValid) {
|
||||
$failedValidator = $validator;
|
||||
}
|
||||
} finally {
|
||||
$wg->done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$wg->wait();
|
||||
|
||||
if (!\is_null($failedValidator)) {
|
||||
$this->count = $failedValidator->count;
|
||||
$this->value = $failedValidator->value;
|
||||
$this->reason = $failedValidator->reason;
|
||||
$this->records = $failedValidator->records;
|
||||
return false;
|
||||
}
|
||||
|
||||
$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 ($record->rdata === $this->target) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
public function getType(): string
|
||||
{
|
||||
return self::TYPE_STRING;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Platform\Modules\Proxy\Http\Rules;
|
||||
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Network\Validator\DNS as ValidatorDNS;
|
||||
use Appwrite\Platform\Action as PlatformAction;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\DNS\Message\Record;
|
||||
use Utopia\Domains\Domain;
|
||||
use Utopia\Logger\Log;
|
||||
use Utopia\System\System;
|
||||
use Utopia\Validator\AnyOf;
|
||||
use Utopia\Validator\Domain as ValidatorDomain;
|
||||
use Utopia\Validator\IP;
|
||||
|
||||
class Action extends PlatformAction
|
||||
{
|
||||
public function __construct(protected string $dnsValidatorClass = ValidatorDNS::class)
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures domain is not in the deny list and is a valid domain
|
||||
*
|
||||
* @param string $domain Domain to validate
|
||||
* @param array $platform Platform configuration which has internal domains
|
||||
* @throws Exception
|
||||
* @return void
|
||||
*/
|
||||
protected function validateDomainRestrictions(string $domain, array $platform): void
|
||||
{
|
||||
$domains = $platform['hostnames'] ?? [];
|
||||
$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://.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
|
||||
@@ -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', ''));
|
||||
|
||||
// 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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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);
|
||||
$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;
|
||||
}
|
||||
|
||||
// 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.');
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
|
||||
@@ -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', ''));
|
||||
|
||||
// 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', ''));
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user