diff --git a/app/controllers/api/proxy.php b/app/controllers/api/proxy.php index e691077adf..e7aa3acb6c 100644 --- a/app/controllers/api/proxy.php +++ b/app/controllers/api/proxy.php @@ -15,176 +15,13 @@ use Utopia\App; use Utopia\Database\Database; use Utopia\Database\Document; use Utopia\Database\Exception\Query as QueryException; -use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Query\Cursor; use Utopia\Database\Validator\UID; use Utopia\Domains\Domain; use Utopia\Logger\Log; use Utopia\System\System; -use Utopia\Validator\Domain as ValidatorDomain; use Utopia\Validator\Text; -use Utopia\Validator\WhiteList; - -App::post('/v1/proxy/rules') - ->groups(['api', 'proxy']) - ->desc('Create rule') - ->label('scope', 'rules.write') - ->label('event', 'rules.[ruleId].create') - ->label('audits.event', 'rule.create') - ->label('audits.resource', 'rule/{response.$id}') - ->label('sdk', new Method( - namespace: 'proxy', - name: 'createRule', - description: '/docs/references/proxy/create-rule.md', - auth: [AuthType::ADMIN], - responses: [ - new SDKResponse( - code: Response::STATUS_CODE_CREATED, - model: Response::MODEL_PROXY_RULE, - ) - ] - )) - ->param('domain', null, new ValidatorDomain(), 'Domain name.') - ->param('resourceType', null, new WhiteList(['api', 'function', 'site']), 'Action definition for the rule. Possible values are "api", "function" and "site"') - ->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true) - ->inject('response') - ->inject('project') - ->inject('queueForCertificates') - ->inject('queueForEvents') - ->inject('dbForPlatform') - ->inject('dbForProject') - ->action(function (string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) { - $mainDomain = System::getEnv('_APP_DOMAIN', ''); - if ($domain === $mainDomain) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your main domain to specific resource. Please use subdomain or a different domain.'); - } - - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - - if ( - ($functionsDomain !== '' && str_ends_with($domain, $functionsDomain)) || - ($sitesDomain !== '' && str_ends_with($domain, $sitesDomain)) - ) { - // TODO: Refactor later - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'You cannot assign your functions or sites domain or their subdomains to a specific resource. Please use a different domain.'); - } - - if ($domain === 'localhost' || $domain === APP_HOSTNAME_INTERNAL) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); - } - - // TODO: @christyjacob remove once we migrate the rules in 1.7.x - if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { - $document = $dbForPlatform->getDocument('rules', md5($domain)); - } else { - $document = $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]); - } - - - if (!$document->isEmpty()) { - if ($document->getAttribute('projectId') === $project->getId()) { - $resourceType = $document->getAttribute('resourceType'); - $resourceId = $document->getAttribute('resourceId'); - $message = "Domain already assigned to '{$resourceType}' service"; - if (!empty($resourceId)) { - $message .= " with ID '{$resourceId}'"; - } - - $message .= '.'; - } else { - $message = 'Domain already assigned to different project.'; - } - - throw new Exception(Exception::RULE_ALREADY_EXISTS, $message); - } - - $resourceInternalId = ''; - - switch ($resourceType) { - case 'function': - if (empty($resourceId)) { - throw new Exception(Exception::FUNCTION_NOT_FOUND); - } - - $function = $dbForProject->getDocument('functions', $resourceId); - - if ($function->isEmpty()) { - throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); - } - - $resourceInternalId = $function->getInternalId(); - break; - case 'site': - if (empty($resourceId)) { - throw new Exception(Exception::SITE_NOT_FOUND); - } - - $site = $dbForProject->getDocument('sites', $resourceId); - - if ($site->isEmpty()) { - throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); - } - - $resourceInternalId = $site->getInternalId(); - break; - } - - try { - $domain = new Domain($domain); - } catch (\Throwable) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); - } - - // TODO: @christyjacob remove once we migrate the rules in 1.7.x - $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); - - $rule = new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain->get(), - 'resourceType' => $resourceType, - 'resourceId' => $resourceId, - 'resourceInternalId' => $resourceInternalId, - 'certificateId' => '', - ]); - - $status = 'created'; - - if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { - $status = 'verified'; - } - - if ($status === 'created') { - $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); - $validator = new CNAME($target->get()); // Verify Domain with DNS records - - if ($validator->isValid($domain->get())) { - $status = 'verifying'; - - $queueForCertificates - ->setDomain(new Document([ - 'domain' => $rule->getAttribute('domain') - ])) - ->trigger(); - } - } - - $rule->setAttribute('status', $status); - $rule = $dbForPlatform->createDocument('rules', $rule); - - $queueForEvents->setParam('ruleId', $rule->getId()); - - $rule->setAttribute('logs', ''); - - $response - ->setStatusCode(Response::STATUS_CODE_CREATED) - ->dynamic($rule, Response::MODEL_PROXY_RULE); - }); App::get('/v1/proxy/rules') ->groups(['api', 'proxy']) @@ -411,34 +248,3 @@ App::patch('/v1/proxy/rules/:ruleId/verification') $response->dynamic($rule, Response::MODEL_PROXY_RULE); }); - -App::get('/v1/proxy/subdomains') - ->desc('Check if subdomain is available') - ->groups(['api', 'proxy']) - ->label('scope', 'rules.read') - ->label('sdk.auth', [APP_AUTH_TYPE_ADMIN]) - ->label('sdk.namespace', 'proxy') - ->label('sdk.method', 'checkSubdomain') - ->label('sdk.description', '/docs/references/proxy/check-subdomain.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_NONE) - ->param('resourceType', null, new WhiteList(['function', 'site']), 'Action definition for the rule. Possible values are "function" and "site"') - ->param('subdomain', '', new Text(256), 'Subdomain name.') - ->inject('response') - ->inject('dbForPlatform') - ->action(function (string $resourceType, string $subdomain, Response $response, Database $dbForPlatform) { - //TODO: Add tests for this endpoint - $resourceDomain = $resourceType === 'site' ? System::getEnv('_APP_DOMAIN_SITES', '') : System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - $domain = $subdomain . '.' . $resourceDomain; - - $document = $dbForPlatform->findOne('rules', [ - Query::equal('domain', [$domain]), - ]); - - if ($document && !$document->isEmpty()) { - throw new Exception(Exception::RULE_ALREADY_EXISTS, 'Subdomain already assigned to different project.'); - } - - $response->noContent(); - }); diff --git a/app/controllers/general.php b/app/controllers/general.php index c9efc90426..a1a5dac3d1 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -737,9 +737,19 @@ App::init() $validator = new Hostname($clients); if ($validator->isValid($origin)) { $refDomainOrigin = $origin; - } else { + } elseif (!empty($origin)) { // Auto-allow domains with linked rule - $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); + if (System::getEnv('_APP_RULES_FORMAT') === 'md5') { + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); + } else { + $rule = Authorization::skip( + fn () => $dbForPlatform->find('rules', [ + Query::equal('domain', [$origin]), + Query::limit(1) + ]) + )[0] ?? new Document(); + } + if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getInternalId()) { $refDomainOrigin = $origin; } diff --git a/docs/references/proxy/create-rule.md b/docs/references/proxy/create-rule.md deleted file mode 100644 index be567b1cc0..0000000000 --- a/docs/references/proxy/create-rule.md +++ /dev/null @@ -1 +0,0 @@ -Create a new proxy rule. \ No newline at end of file diff --git a/src/Appwrite/Platform/Appwrite.php b/src/Appwrite/Platform/Appwrite.php index 8633da6cb6..be6ddbae8a 100644 --- a/src/Appwrite/Platform/Appwrite.php +++ b/src/Appwrite/Platform/Appwrite.php @@ -5,6 +5,7 @@ namespace Appwrite\Platform; use Appwrite\Platform\Modules\Console; use Appwrite\Platform\Modules\Core; use Appwrite\Platform\Modules\Functions; +use Appwrite\Platform\Modules\Proxy; use Appwrite\Platform\Modules\Sites; use Utopia\Platform\Platform; @@ -16,5 +17,6 @@ class Appwrite extends Platform $this->addModule(new Functions\Module()); $this->addModule(new Sites\Module()); $this->addModule(new Console\Module()); + $this->addModule(new Proxy\Module()); } } diff --git a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php index 0bf08ea412..0507c68068 100644 --- a/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php +++ b/src/Appwrite/Platform/Modules/Functions/Http/Functions/Create.php @@ -6,7 +6,6 @@ use Appwrite\Event\Event; use Appwrite\Event\Validator\FunctionEvent; use Appwrite\Extend\Exception; use Appwrite\Functions\Validator\RuntimeSpecification; -use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -14,7 +13,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Task\Validator\Cron; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Model\Rule; use Utopia\Abuse\Abuse; use Utopia\App; use Utopia\Config\Config; @@ -229,72 +227,6 @@ class Create extends Base $function = $dbForProject->updateDocument('functions', $function->getId(), $function); - $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); - if (!empty($functionsDomain)) { - $routeSubdomain = ID::unique(); - $domain = "{$routeSubdomain}.{$functionsDomain}"; - $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique(); - - $rule = Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => $ruleId, - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'resourceType' => 'function', - 'resourceId' => $function->getId(), - 'resourceInternalId' => $function->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); - - /** Trigger Webhook */ - $ruleModel = new Rule(); - $ruleCreate = - $queueForEvents - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME); - - $ruleCreate - ->setProject($project) - ->setEvent('rules.[ruleId].create') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); - - /** Trigger Functions */ - $ruleCreate - ->setClass(Event::FUNCTIONS_CLASS_NAME) - ->setQueue(Event::FUNCTIONS_QUEUE_NAME) - ->trigger(); - - /** Trigger realtime event */ - $allEvents = Event::generateEvents('rules.[ruleId].create', [ - 'ruleId' => $rule->getId(), - ]); - $target = Realtime::fromPayload( - // Pass first, most verbose event pattern - event: $allEvents[0], - payload: $rule, - project: $project - ); - Realtime::send( - projectId: 'console', - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - Realtime::send( - projectId: $project->getId(), - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - } - $queueForEvents->setParam('functionId', $function->getId()); $response diff --git a/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Create.php b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Create.php new file mode 100644 index 0000000000..b989310a3d --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Http/Rules/Create.php @@ -0,0 +1,179 @@ +setHttpMethod(Action::HTTP_REQUEST_METHOD_POST) + ->setHttpPath('/v1/proxy/rules') + ->groups(['api', 'proxy']) + ->desc('Create rule') + ->label('scope', 'rules.write') + ->label('event', 'rules.[ruleId].create') + ->label('audits.event', 'rule.create') + ->label('audits.resource', 'rule/{response.$id}') + ->label('sdk', new Method( + namespace: 'proxy', + name: 'createRule', + description: <<label('abuse-limit', 10) + ->label('abuse-key', 'userId:{userId}, url:{url}') + ->label('abuse-time', 60) + ->param('domain', null, new ValidatorDomain(), 'Domain name.') + ->param('resourceType', null, new WhiteList(['api', 'function', 'site']), 'Action definition for the rule. Possible values are "api", "function" and "site"') + ->param('resourceId', '', new UID(), 'ID of resource for the action type. If resourceType is "api", leave empty. If resourceType is "function", provide ID of the function.', true) + ->inject('response') + ->inject('project') + ->inject('queueForCertificates') + ->inject('queueForEvents') + ->inject('dbForPlatform') + ->inject('dbForProject') + ->callback([$this, 'action']); + } + + public function action(string $domain, string $resourceType, string $resourceId, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject) + { + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + if (in_array($domain, $deniedDomains, true)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please pick another one.'); + } + + $resourceInternalId = ''; + + switch ($resourceType) { + case 'function': + case 'site': + if (empty($resourceId)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'resourceId cannot be empty for resourceType "' . $resourceType . '".'); + } + + $expectedDomain = ($resourceType === 'function') ? $functionsDomain : $sitesDomain; + if (!\str_ends_with($domain, $expectedDomain)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain must end with ' . $expectedDomain . ' for resourceType "' . $resourceType . '".'); + } + + $collection = ($resourceType === 'function') ? 'functions' : 'sites'; + $document = $dbForProject->getDocument($collection, $resourceId); + + if ($document->isEmpty()) { + throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND); + } + + $resourceInternalId = $document->getInternalId(); + break; + case 'api': + if (\str_ends_with($domain, $functionsDomain) || \str_ends_with($domain, $sitesDomain)) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain must not end with ' . $functionsDomain . ' or ' . $sitesDomain . ' for resourceType "api".'); + } + break; + } + + try { + $domain = new Domain($domain); + } catch (\Throwable) { + throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Domain may not start with http:// or https://.'); + } + + // TODO: @christyjacob remove once we migrate the rules in 1.7.x + $ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique(); + + try { + $rule = new Document([ + '$id' => $ruleId, + 'projectId' => $project->getId(), + 'projectInternalId' => $project->getInternalId(), + 'domain' => $domain->get(), + 'resourceType' => $resourceType, + 'resourceId' => $resourceId, + 'resourceInternalId' => $resourceInternalId, + 'certificateId' => '', + ]); + } catch (\Throwable $e) { + if ($e->getCode() === Exception::DOCUMENT_ALREADY_EXISTS) { + throw new Exception(Exception::RULE_ALREADY_EXISTS); + } + + throw new Exception(Exception::GENERAL_SERVER_ERROR, 'An unexpected error occurred: ' . $e->getMessage()); + } + + $status = 'created'; + + if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) { + $status = 'verified'; + } + + if ($status === 'created') { + $target = new Domain(System::getEnv('_APP_DOMAIN_TARGET', '')); + $validator = new CNAME($target->get()); // Verify Domain with DNS records + + if ($validator->isValid($domain->get())) { + $status = 'verifying'; + + $queueForCertificates + ->setDomain(new Document([ + 'domain' => $rule->getAttribute('domain') + ])) + ->trigger(); + } + } + + $rule->setAttribute('status', $status); + $rule = $dbForPlatform->createDocument('rules', $rule); + + $queueForEvents->setParam('ruleId', $rule->getId()); + + $response + ->setStatusCode(Response::STATUS_CODE_CREATED) + ->dynamic($rule, Response::MODEL_PROXY_RULE); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Module.php b/src/Appwrite/Platform/Modules/Proxy/Module.php new file mode 100644 index 0000000000..cd8f6f86dc --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Module.php @@ -0,0 +1,14 @@ +addService('http', new Http()); + } +} diff --git a/src/Appwrite/Platform/Modules/Proxy/Services/Http.php b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php new file mode 100644 index 0000000000..bc564f3714 --- /dev/null +++ b/src/Appwrite/Platform/Modules/Proxy/Services/Http.php @@ -0,0 +1,16 @@ +type = Service::TYPE_HTTP; + // Rules + $this->addAction(CreateRule::getName(), new CreateRule()); + } +} diff --git a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php index 52fa8e47a2..6072489310 100644 --- a/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php +++ b/src/Appwrite/Platform/Modules/Sites/Http/Sites/Create.php @@ -4,7 +4,6 @@ namespace Appwrite\Platform\Modules\Sites\Http\Sites; use Appwrite\Event\Event; use Appwrite\Extend\Exception; -use Appwrite\Messaging\Adapter\Realtime; use Appwrite\Platform\Modules\Compute\Base; use Appwrite\SDK\AuthType; use Appwrite\SDK\Method; @@ -12,7 +11,6 @@ use Appwrite\SDK\Response as SDKResponse; use Appwrite\Sites\Validator\FrameworkSpecification; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Response; -use Appwrite\Utopia\Response\Model\Rule; use Utopia\App; use Utopia\Config\Config; use Utopia\Database\Database; @@ -20,7 +18,6 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; -use Utopia\Database\Validator\Authorization; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; use Utopia\System\System; @@ -71,7 +68,6 @@ class Create extends Base ->param('installCommand', '', new Text(8192, 0), 'Install Command.', true) ->param('buildCommand', '', new Text(8192, 0), 'Build Command.', true) ->param('outputDirectory', '', new Text(8192, 0), 'Output Directory for site.', true) - ->param('subdomain', '', new CustomId(), 'Unique custom sub-domain. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.', true) ->param('buildRuntime', '', new WhiteList(array_keys(Config::getParam('runtimes')), true), 'Runtime to use during build step.') ->param('adapter', '', new Text(8192, 0), 'Framework adapter. Allows: static, ssr', true) ->param('installationId', '', new Text(128, 0), 'Appwrite Installation ID for VCS (Version Control System) deployment.', true) @@ -94,7 +90,7 @@ class Create extends Base ->callback([$this, 'action']); } - public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $subdomain, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform) + public function action(string $siteId, string $name, string $framework, bool $enabled, int $timeout, string $installCommand, string $buildCommand, string $outputDirectory, string $buildRuntime, string $adapter, string $installationId, ?string $fallbackFile, string $providerRepositoryId, string $providerBranch, bool $providerSilentMode, string $providerRootDirectory, string $specification, Response $response, Database $dbForProject, Document $project, Event $queueForEvents, Database $dbForPlatform) { if (!empty($adapter)) { $configFramework = Config::getParam('frameworks')[$framework] ?? []; @@ -105,21 +101,6 @@ class Create extends Base } } - $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); - $routeSubdomain = ''; - $domain = ''; - - if (!empty($sitesDomain)) { - $routeSubdomain = $subdomain ?: ID::unique(); - $domain = "{$routeSubdomain}.{$sitesDomain}"; - - $subdomain = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', \md5($domain))); - - if ($subdomain && !$subdomain->isEmpty()) { - throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'Subdomain already exists. Please choose a different subdomain.'); - } - } - $siteId = ($siteId == 'unique()') ? ID::unique() : $siteId; $allowList = \array_filter(\explode(',', System::getEnv('_APP_SITES_FRAMEWORKS', ''))); @@ -195,67 +176,6 @@ class Create extends Base $site = $dbForProject->updateDocument('sites', $site->getId(), $site); - if (!empty($sitesDomain)) { - $rule = Authorization::skip( - fn () => $dbForPlatform->createDocument('rules', new Document([ - '$id' => \md5($domain), - 'projectId' => $project->getId(), - 'projectInternalId' => $project->getInternalId(), - 'domain' => $domain, - 'resourceType' => 'site', - 'resourceId' => $site->getId(), - 'resourceInternalId' => $site->getInternalId(), - 'status' => 'verified', - 'certificateId' => '', - ])) - ); - - /** Trigger Webhook */ - $ruleModel = new Rule(); - $ruleCreate = - $queueForEvents - ->setClass(Event::WEBHOOK_CLASS_NAME) - ->setQueue(Event::WEBHOOK_QUEUE_NAME); - - $ruleCreate - ->setProject($project) - ->setEvent('rules.[ruleId].create') - ->setParam('ruleId', $rule->getId()) - ->setPayload($rule->getArrayCopy(array_keys($ruleModel->getRules()))) - ->trigger(); - - /** Trigger Sites */ - $ruleCreate - ->setClass(Event::SITES_CLASS_NAME) - ->setQueue(Event::SITES_QUEUE_NAME) - ->trigger(); - - /** Trigger realtime event */ - $allEvents = Event::generateEvents('rules.[ruleId].create', [ - 'ruleId' => $rule->getId(), - ]); - $target = Realtime::fromPayload( - // Pass first, most verbose event pattern - event: $allEvents[0], - payload: $rule, - project: $project - ); - Realtime::send( - projectId: 'console', - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - Realtime::send( - projectId: $project->getId(), - payload: $rule->getArrayCopy(), - events: $allEvents, - channels: $target['channels'], - roles: $target['roles'] - ); - } - $queueForEvents->setParam('siteId', $site->getId()); $response diff --git a/tests/e2e/General/UsageTest.php b/tests/e2e/General/UsageTest.php index 74ae1c00bc..b053e4b9cd 100644 --- a/tests/e2e/General/UsageTest.php +++ b/tests/e2e/General/UsageTest.php @@ -12,10 +12,12 @@ use Tests\E2E\Scopes\ProjectCustom; use Tests\E2E\Scopes\Scope; use Tests\E2E\Scopes\SideServer; use Tests\E2E\Services\Functions\FunctionsBase; +use Utopia\Database\Helpers\ID; use Utopia\Database\Helpers\Permission; use Utopia\Database\Helpers\Role; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class UsageTest extends Scope { @@ -1090,22 +1092,25 @@ class UsageTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), + $rule = $this->client->call( + Client::METHOD_POST, + '/proxy/rules', + array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), + [ + 'domain' => 'test-' . ID::unique() . System::getEnv('_APP_DOMAIN_FUNCTIONS'), + 'resourceType' => 'function', + 'resourceId' => $functionId, ], - ]); + ); - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertNotEmpty($rule['body']['$id']); + $this->assertNotEmpty($rule['body']['domain']); - $domain = $rules['body']['rules'][0]['domain']; + $domain = $rule['body']['domain']; $response = $this->client->call( Client::METHOD_GET, diff --git a/tests/e2e/Services/Functions/FunctionsBase.php b/tests/e2e/Services/Functions/FunctionsBase.php index e99aedc93e..ce5df8b746 100644 --- a/tests/e2e/Services/Functions/FunctionsBase.php +++ b/tests/e2e/Services/Functions/FunctionsBase.php @@ -6,6 +6,9 @@ use Appwrite\Tests\Async; use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; +use Utopia\Database\Helpers\ID; +use Utopia\Database\Query; +use Utopia\System\System; trait FunctionsBase { @@ -255,4 +258,47 @@ trait FunctionsBase return $function; } + + protected function setupFunctionDomain(string $functionId, string $subdomain = ''): string + { + $subdomain = $subdomain ? $subdomain : ID::unique(); + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_FUNCTIONS', ''), + 'resourceType' => 'function', + 'resourceId' => $functionId, + ]); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertNotEmpty($rule['body']['$id']); + $this->assertNotEmpty($rule['body']['domain']); + + $domain = $rule['body']['domain']; + + return $domain; + } + + protected function getFunctionDomain(string $functionId): string + { + $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'queries' => [ + Query::equal('resourceId', [$functionId])->toString(), + Query::equal('resourceType', ['function'])->toString(), + ], + ]); + + $this->assertEquals(200, $rules['headers']['status-code']); + $this->assertGreaterThanOrEqual(1, $rules['body']['total']); + $this->assertGreaterThanOrEqual(1, \count($rules['body']['rules'])); + $this->assertNotEmpty($rules['body']['rules'][0]['domain']); + + $domain = $rules['body']['rules'][0]['domain']; + + return $domain; + } } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 8975a1a5dc..f941c0658a 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1654,22 +1654,7 @@ class FunctionsCustomServerTest extends Scope 'execute' => ['any'] ]); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -1730,22 +1715,7 @@ class FunctionsCustomServerTest extends Scope 'execute' => ['any'] ]); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -1780,22 +1750,7 @@ class FunctionsCustomServerTest extends Scope 'execute' => ['any'] ]); - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertEquals(1, $rules['body']['total']); - $this->assertCount(1, $rules['body']['rules']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->setupFunctionDomain($functionId); $this->setupDeployment($functionId, [ 'entrypoint' => 'index.php', @@ -1915,6 +1870,8 @@ class FunctionsCustomServerTest extends Scope $functionId = $function['body']['$id'] ?? ''; + $domain = $this->setupFunctionDomain($functionId); + $this->setupDeployment($functionId, [ 'code' => $this->packageFunction('node'), 'activate' => true @@ -1949,20 +1906,7 @@ class FunctionsCustomServerTest extends Scope }, 10000, 500); // Domain Executions test - $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ - 'content-type' => 'application/json', - 'x-appwrite-project' => $this->getProject()['$id'], - ], $this->getHeaders()), [ - 'queries' => [ - Query::equal('resourceId', [$functionId])->toString(), - Query::equal('resourceType', ['function'])->toString(), - ], - ]); - - $this->assertEquals(200, $rules['headers']['status-code']); - $this->assertNotEmpty($rules['body']['rules'][0]['domain']); - - $domain = $rules['body']['rules'][0]['domain']; + $domain = $this->getFunctionDomain($functionId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); diff --git a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php index f81290e707..60ae7e0bbb 100644 --- a/tests/e2e/Services/Projects/ProjectsCustomServerTest.php +++ b/tests/e2e/Services/Projects/ProjectsCustomServerTest.php @@ -31,11 +31,25 @@ class ProjectsCustomServerTest extends Scope $this->assertEquals(201, $response['headers']['status-code']); + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ + 'resourceType' => 'api', + 'domain' => 'abc.test.io', + ]); + + $this->assertEquals(201, $response['headers']['status-code']); + + // duplicate rule + $response2 = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ + 'resourceType' => 'api', + 'domain' => 'abc.test.io', + ]); + + $this->assertEquals(409, $response2['headers']['status-code']); + $response = $this->client->call(Client::METHOD_DELETE, '/proxy/rules/' . $response['body']['$id'], $headers); $this->assertEquals(204, $response['headers']['status-code']); - // prevent functions domain $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ @@ -45,7 +59,7 @@ class ProjectsCustomServerTest extends Scope $this->assertEquals(400, $response['headers']['status-code']); - // prevent sites domain + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ @@ -54,5 +68,42 @@ class ProjectsCustomServerTest extends Scope ]); $this->assertEquals(400, $response['headers']['status-code']); + + // prevent functions domain + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ + 'resourceType' => 'function', + 'domain' => $functionsDomain, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + // prevent sites domain + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ + 'resourceType' => 'site', + 'domain' => $sitesDomain, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + + $mainDomain = System::getEnv('_APP_DOMAIN', ''); + $sitesDomain = System::getEnv('_APP_DOMAIN_SITES', ''); + $functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', ''); + + $deniedDomains = [ + $mainDomain, + $sitesDomain, + $functionsDomain, + 'localhost', + APP_HOSTNAME_INTERNAL, + ]; + + foreach ($deniedDomains as $deniedDomain) { + $response = $this->client->call(Client::METHOD_POST, '/proxy/rules', $headers, [ + 'resourceType' => 'api', + 'domain' => $deniedDomain, + ]); + + $this->assertEquals(400, $response['headers']['status-code']); + } } } diff --git a/tests/e2e/Services/Sites/SitesBase.php b/tests/e2e/Services/Sites/SitesBase.php index bc7b6d2722..4fcd34572d 100644 --- a/tests/e2e/Services/Sites/SitesBase.php +++ b/tests/e2e/Services/Sites/SitesBase.php @@ -6,7 +6,9 @@ use Appwrite\Tests\Async; use CURLFile; use Tests\E2E\Client; use Utopia\CLI\Console; +use Utopia\Database\Helpers\ID; use Utopia\Database\Query; +use Utopia\System\System; trait SitesBase { @@ -267,6 +269,27 @@ trait SitesBase return $site; } + protected function setupSiteDomain(string $siteId, string $subdomain = ''): string + { + $subdomain = $subdomain ? $subdomain : ID::unique(); + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + 'resourceType' => 'site', + 'resourceId' => $siteId, + ]); + + $this->assertEquals(201, $rule['headers']['status-code']); + $this->assertNotEmpty($rule['body']['$id']); + $this->assertNotEmpty($rule['body']['domain']); + + $domain = $rule['body']['domain']; + + return $domain; + } + protected function getSiteDomain(string $siteId): string { $rules = $this->client->call(Client::METHOD_GET, '/proxy/rules', array_merge([ diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index b01d31ada7..eefa0ddcbb 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -11,6 +11,7 @@ use Utopia\Database\Document; use Utopia\Database\Helpers\ID; use Utopia\Database\Query; use Utopia\Database\Validator\Datetime as DatetimeValidator; +use Utopia\System\System; class SitesCustomServerTest extends Scope { @@ -73,13 +74,12 @@ class SitesCustomServerTest extends Scope 'framework' => 'other', 'buildRuntime' => 'ssr-22', 'outputDirectory' => './', - 'subdomain' => 'test-site', 'fallbackFile' => null, ]); $this->assertNotEmpty($siteId); - $rule = $this->getSiteDomain($siteId); + $rule = $this->setupSiteDomain($siteId); $response = $this->client->call(Client::METHOD_GET, '/console/resources', [ 'origin' => 'http://localhost', @@ -289,6 +289,8 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($siteId); + $domain = $this->setupSiteDomain($siteId); + $secretVariable = $this->createVariable($siteId, [ 'key' => 'name', 'value' => 'Appwrite', @@ -1243,7 +1245,7 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($site['body']['deploymentId']); }, 50000, 500); - $domain = $this->getSiteDomain($siteId); + $domain = $this->setupSiteDomain($siteId); $proxyClient = new Client(); $proxyClient->setEndpoint('http://' . $domain); @@ -1270,8 +1272,6 @@ class SitesCustomServerTest extends Scope public function testSiteDomainReclaiming(): void { - $subdomain = 'startup' . \uniqid(); - $siteId = $this->setupSite([ 'siteId' => ID::unique(), 'name' => 'Startup site', @@ -1282,11 +1282,13 @@ class SitesCustomServerTest extends Scope 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', - 'subdomain' => $subdomain ]); $this->assertNotEmpty($siteId); + $subdomain = 'startup' . \uniqid(); + $domain = $this->setupSiteDomain($siteId, $subdomain); + $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => 'true' @@ -1306,7 +1308,7 @@ class SitesCustomServerTest extends Scope $this->assertEquals(200, $response['headers']['status-code']); $this->assertStringNotContainsString("This domain is not connected to any Appwrite resource yet", $response['body']); - $site = $this->createSite([ + $site2 = $this->createSite([ 'siteId' => ID::unique(), 'name' => 'Startup 2 site', 'framework' => 'other', @@ -1316,11 +1318,21 @@ class SitesCustomServerTest extends Scope 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', - 'subdomain' => $subdomain ]); - $this->assertEquals(400, $site['headers']['status-code']); - $this->assertStringContainsString("Subdomain already exists.", $site['body']['message']); + $siteId2 = $site2['body']['$id']; + + $rule = $this->client->call(Client::METHOD_POST, '/proxy/rules', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'domain' => $subdomain . '.' . System::getEnv('_APP_DOMAIN_SITES', ''), + 'resourceType' => 'site', + 'resourceId' => $siteId2, + ]); + + $this->assertEquals(409, $rule['headers']['status-code']); + $this->assertStringContainsString("Document with the requested ID already exists. Try again with a different ID or use ID.unique() to generate a unique ID.", $rule['body']['message']); $this->cleanupSite($siteId); @@ -1356,10 +1368,16 @@ class SitesCustomServerTest extends Scope 'buildCommand' => '', 'installCommand' => '', 'fallbackFile' => '', - 'subdomain' => $subdomain ]); $this->assertEquals(201, $site['headers']['status-code']); + $this->assertNotEmpty($site['body']['$id']); + + $siteId = $site['body']['$id']; + + $domain = $this->setupSiteDomain($siteId, $subdomain); + + $this->assertNotEmpty($domain); $this->cleanupSite($site['body']['$id']); } @@ -1380,6 +1398,8 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($siteId); + $domain = $this->setupSiteDomain($siteId); + $deploymentId = $this->setupDeployment($siteId, [ 'code' => $this->packageSite('static'), 'activate' => 'true' @@ -1443,6 +1463,7 @@ class SitesCustomServerTest extends Scope $this->assertNotEmpty($siteId); + $this->setupSiteDomain($siteId, $subdomain); $domain = $this->getSiteDomain($siteId); $this->assertNotEmpty($domain);