From ec05a3d668b1492e63e15cb187661618f32b39bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 13 Feb 2025 19:57:41 +0100 Subject: [PATCH 1/3] Enable CORS for sites --- app/controllers/general.php | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index c8beefec2d..f62d72af33 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -123,7 +123,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $type = $rule->getAttribute('resourceType'); if ($type === 'function' || $type === 'site' || $type === 'deployment') { - $resourceCollection = match($type) { + $resourceCollection = match ($type) { 'function' => 'functions', 'site' => 'sites', 'deployment' => 'deployments', @@ -198,7 +198,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw throw new AppwriteException(AppwriteException::GENERAL_RESOURCE_BLOCKED); } - $version = match($type) { + $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), 'site' => 'v4', 'deployment' => 'v4' @@ -207,7 +207,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $runtimes = Config::getParam($version === 'v2' ? 'runtimes-v2' : 'runtimes', []); $spec = Config::getParam('runtime-specifications')[$resource->getAttribute('specification', APP_COMPUTE_SPECIFICATION_DEFAULT)]; - $runtime = match($type) { + $runtime = match ($type) { 'function' => $runtimes[$resource->getAttribute('runtime')] ?? null, 'site' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, 'deployment' => $runtimes[$resource->getAttribute('buildRuntime')] ?? null, @@ -222,7 +222,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw throw new AppwriteException(AppwriteException::FUNCTION_RUNTIME_UNSUPPORTED, 'Runtime "' . $resource->getAttribute('runtime', '') . '" is not supported'); } - $deploymentId = match($type) { + $deploymentId = match ($type) { 'function' => $resource->getAttribute('deployment', ''), 'site' => $resource->getAttribute('deploymentId', ''), 'deployment' => $subResource->getId() @@ -388,12 +388,12 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw /** Execute function */ $executor = new Executor(System::getEnv('_APP_EXECUTOR_HOST')); try { - $version = match($type) { + $version = match ($type) { 'function' => $resource->getAttribute('version', 'v2'), 'site' => 'v4', 'deployment' => 'v4' }; - $entrypoint = match($type) { + $entrypoint = match ($type) { 'function' => $deployment->getAttribute('entrypoint', ''), 'site' => '', 'deployment' => '' @@ -420,7 +420,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw $runtimeEntrypoint = 'cp /tmp/code.tar.gz /mnt/code/code.tar.gz && nohup helpers/start.sh "' . $startCommand . '"'; } - $entrypoint = match($type) { + $entrypoint = match ($type) { 'function' => $deployment->getAttribute('entrypoint', ''), 'site' => '', 'deployment' => '' @@ -472,8 +472,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw if ($type === 'function') { $execution - ->setAttribute('status', 'failed') - ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); + ->setAttribute('status', 'failed') + ->setAttribute('errors', $th->getMessage() . '\nError Code: ' . $th->getCode()); } Console::error($th->getMessage()); @@ -540,8 +540,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw ->addMetric(METRIC_EXECUTIONS_MB_SECONDS, (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ->addMetric(str_replace('{functionInternalId}', $resource->getInternalId(), METRIC_FUNCTION_ID_EXECUTIONS_MB_SECONDS), (int)(($spec['memory'] ?? APP_COMPUTE_MEMORY_DEFAULT) * $execution->getAttribute('duration', 0) * ($spec['cpus'] ?? APP_COMPUTE_CPUS_DEFAULT))) ->setProject($project) - ->trigger() - ; + ->trigger(); return true; } elseif ($type === 'api') { @@ -720,6 +719,12 @@ App::init() $validator = new Hostname($clients); if ($validator->isValid($origin)) { $refDomainOrigin = $origin; + } else { + // Auto-allow domains with linked rule + $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); + if (!$rule->isEmpty()) { + $refDomainOrigin = $origin; + } } $refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : ''); @@ -769,7 +774,7 @@ App::init() $response->addFilter(new ResponseV18()); } if (version_compare($responseFormat, APP_VERSION_STABLE, '>')) { - $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is ". APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); + $response->addHeader('X-Appwrite-Warning', "The current SDK is built for Appwrite " . $responseFormat . ". However, the current Appwrite server version is " . APP_VERSION_STABLE . ". Please downgrade your SDK to match the Appwrite version: https://appwrite.io/docs/sdks"); } } From a10882d7eef2be04621504f66e94a69236f0ff56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Thu, 13 Feb 2025 19:59:29 +0100 Subject: [PATCH 2/3] Update general.php --- app/controllers/general.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index f62d72af33..222b842207 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -722,7 +722,7 @@ App::init() } else { // Auto-allow domains with linked rule $rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($origin))); - if (!$rule->isEmpty()) { + if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getInternalId()) { $refDomainOrigin = $origin; } } From 43f0dc08464e365116a86b3ed051dd41af94b356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 14 Feb 2025 09:47:37 +0100 Subject: [PATCH 3/3] Add CORS tests --- .../Services/Sites/SitesCustomServerTest.php | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/e2e/Services/Sites/SitesCustomServerTest.php b/tests/e2e/Services/Sites/SitesCustomServerTest.php index 38f9a81452..5f54e7250d 100644 --- a/tests/e2e/Services/Sites/SitesCustomServerTest.php +++ b/tests/e2e/Services/Sites/SitesCustomServerTest.php @@ -1346,5 +1346,61 @@ class SitesCustomServerTest extends Scope $this->cleanupSite($site['body']['$id']); } + public function testSiteCors(): void + { + // Create rule together with site + $subdomain = 'startup' . \uniqid(); + + $siteId = $this->setupSite([ + 'siteId' => ID::unique(), + 'name' => 'Startup site', + 'framework' => 'other', + 'adapter' => 'static', + 'buildRuntime' => 'static-1', + 'outputDirectory' => './', + 'buildCommand' => '', + 'installCommand' => '', + 'fallbackFile' => '', + 'subdomain' => $subdomain + ]); + + $this->assertNotEmpty($siteId); + + $domain = $this->getSiteDomain($siteId); + + $this->assertNotEmpty($domain); + + $url = 'http://' . $domain; + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'referer' => $url, + 'origin' => $url + ])); + + $this->assertEquals($url, $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => 'unknown', + 'referer' => $url, + 'origin' => $url + ])); + + $this->assertNotEquals($url, $response['headers']['access-control-allow-origin']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + + $response = $this->client->call(Client::METHOD_GET, '/account', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'referer' => 'http://unknown.com', + 'origin' => 'http://unknown.com' + ])); + + $this->assertNotEquals($url, $response['headers']['access-control-allow-origin']); + $this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']); + } + // TODO: Add tests for deletion of resources when site is deleted }