From 94ff3baa9c559d67b73075589355d2be31c08ba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 27 Oct 2023 10:26:46 +0200 Subject: [PATCH 1/6] Fix cookie headers --- app/controllers/general.php | 15 +++- .../Functions/FunctionsCustomServerTest.php | 88 +++++++++++++++++++ .../resources/functions/php-cookie/index.php | 6 ++ 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 tests/resources/functions/php-cookie/index.php diff --git a/app/controllers/general.php b/app/controllers/general.php index 0a67148576..448f23c875 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -117,12 +117,23 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $path .= '?' . $query; } + $swooleHeaders = $swooleRequest->header; + + $cookieHeaders = []; + foreach ($swooleRequest->cookie as $key => $value) { + $cookieHeaders[] = "{$key}={$value}"; + } + + if(!empty($cookieHeaders)) { + $swooleHeaders['cookie'] = \implode('; ', $cookieHeaders); + } + $body = \json_encode([ 'async' => false, 'body' => $swooleRequest->getContent() ?? '', 'method' => $swooleRequest->server['request_method'], 'path' => $path, - 'headers' => $swooleRequest->header + 'headers' => $swooleHeaders ]); $headers = [ @@ -406,7 +417,7 @@ App::init() * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + if ($request->getProtocol() !== 'https' && ($swooleHeaders['host'] ?? '') !== 'localhost' && ($swooleHeaders['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index cdc9ec846f..58c89f5687 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1344,4 +1344,92 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } + + public function testCookieExecution() + { + $timeout = 5; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-cookie/code.tar.gz"; + $this->packageCode('php-cookie'); + + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test PHP Cookie executions', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => $timeout, + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['headers']['status-code']); + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.php', + 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + // Poll until deployment is built + while (true) { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + if ( + $deployment['headers']['status-code'] >= 400 + || \in_array($deployment['body']['status'], ['ready', 'failed']) + ) { + break; + } + + \sleep(1); + } + + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $deployment['headers']['status-code']); + + // Wait a little for activation to finish + sleep(5); + + $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=value4'; + + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'async' => false, + 'headers' => [ + 'cookie' => $cookie + ] + ]); + + $this->assertEquals(201, $execution['headers']['status-code']); + $this->assertEquals('completed', $execution['body']['status']); + $this->assertEquals(200, $execution['body']['responseStatusCode']); + $this->assertEquals($cookie, $execution['body']['responseBody']); + + // Cleanup : Delete function + $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], []); + + $this->assertEquals(204, $response['headers']['status-code']); + } } diff --git a/tests/resources/functions/php-cookie/index.php b/tests/resources/functions/php-cookie/index.php new file mode 100644 index 0000000000..2812dbc24e --- /dev/null +++ b/tests/resources/functions/php-cookie/index.php @@ -0,0 +1,6 @@ +log($context->req->headers); + return $context->res->send($context->req->headers['cookie'] ?? ''); +}; From 740d9def8e8ebbc25b47017db5c6f5423b621865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 27 Oct 2023 10:33:08 +0200 Subject: [PATCH 2/6] Linter fix --- app/controllers/general.php | 2 +- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 448f23c875..14b074d21d 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -124,7 +124,7 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $cookieHeaders[] = "{$key}={$value}"; } - if(!empty($cookieHeaders)) { + if (!empty($cookieHeaders)) { $swooleHeaders['cookie'] = \implode('; ', $cookieHeaders); } diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index 58c89f5687..b5030dfd49 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1405,9 +1405,9 @@ class FunctionsCustomServerTest extends Scope // Wait a little for activation to finish sleep(5); - + $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=value4'; - + $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', 'x-appwrite-project' => $this->getProject()['$id'], From 84559b8f56be312db0d0a7b1bb34df27851ee680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 27 Oct 2023 12:01:37 +0200 Subject: [PATCH 3/6] Improve cookie test --- tests/e2e/Services/Functions/FunctionsCustomServerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index b5030dfd49..a816ba1c9f 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1406,7 +1406,7 @@ class FunctionsCustomServerTest extends Scope // Wait a little for activation to finish sleep(5); - $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=value4'; + $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; $execution = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/executions', array_merge([ 'content-type' => 'application/json', From b2d385944a9f2aaf46cc5def8977751cb77aa5b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 27 Oct 2023 15:33:26 +0200 Subject: [PATCH 4/6] Add function domain cookie test --- tests/e2e/Scopes/ProjectCustom.php | 2 + .../Functions/FunctionsCustomServerTest.php | 100 ++++++++++++++++++ .../resources/functions/php-cookie/index.php | 1 - 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/tests/e2e/Scopes/ProjectCustom.php b/tests/e2e/Scopes/ProjectCustom.php index 408b8c8fe7..5f7bf85d0e 100644 --- a/tests/e2e/Scopes/ProjectCustom.php +++ b/tests/e2e/Scopes/ProjectCustom.php @@ -81,6 +81,8 @@ trait ProjectCustom 'locale.read', 'avatars.read', 'health.read', + 'rules.read', + 'rules.write' ], ]); diff --git a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php index a816ba1c9f..e39c6573e4 100644 --- a/tests/e2e/Services/Functions/FunctionsCustomServerTest.php +++ b/tests/e2e/Services/Functions/FunctionsCustomServerTest.php @@ -1432,4 +1432,104 @@ class FunctionsCustomServerTest extends Scope $this->assertEquals(204, $response['headers']['status-code']); } + + public function testFunctionsDomain() + { + $timeout = 5; + $code = realpath(__DIR__ . '/../../../resources/functions') . "/php-cookie/code.tar.gz"; + $this->packageCode('php-cookie'); + + $function = $this->client->call(Client::METHOD_POST, '/functions', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'functionId' => ID::unique(), + 'name' => 'Test PHP Cookie executions', + 'runtime' => 'php-8.0', + 'entrypoint' => 'index.php', + 'timeout' => $timeout, + 'execute' => ['any'] + ]); + + $functionId = $function['body']['$id'] ?? ''; + + $this->assertEquals(201, $function['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' => [ 'equal("resourceId", "' . $functionId . '")', 'equal("resourceType", "function")' ] + ]); + + $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']; + + $deployment = $this->client->call(Client::METHOD_POST, '/functions/' . $functionId . '/deployments', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'entrypoint' => 'index.php', + 'code' => new CURLFile($code, 'application/x-gzip', basename($code)), + 'activate' => true + ]); + + $deploymentId = $deployment['body']['$id'] ?? ''; + $this->assertEquals(202, $deployment['headers']['status-code']); + + // Poll until deployment is built + while (true) { + $deployment = $this->client->call(Client::METHOD_GET, '/functions/' . $function['body']['$id'] . '/deployments/' . $deploymentId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + if ( + $deployment['headers']['status-code'] >= 400 + || \in_array($deployment['body']['status'], ['ready', 'failed']) + ) { + break; + } + + \sleep(1); + } + + $deployment = $this->client->call(Client::METHOD_PATCH, '/functions/' . $functionId . '/deployments/' . $deploymentId, array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), []); + + $this->assertEquals(200, $deployment['headers']['status-code']); + + // Wait a little for activation to finish + sleep(5); + + $cookie = 'cookieName=cookieValue; cookie2=value2; cookie3=value=3; cookie4=val:ue4; cookie5=value5'; + + $proxyClient = new Client(); + $proxyClient->setEndpoint('http://' . $domain); + + $response = $proxyClient->call(Client::METHOD_GET, '/', array_merge([ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'cookie' => $cookie + ])); + + $this->assertEquals(200, $response['headers']['status-code']); + $this->assertEquals($cookie, $response['body']); + + // Cleanup : Delete function + $response = $this->client->call(Client::METHOD_DELETE, '/functions/' . $functionId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], []); + + $this->assertEquals(204, $response['headers']['status-code']); + } } diff --git a/tests/resources/functions/php-cookie/index.php b/tests/resources/functions/php-cookie/index.php index 2812dbc24e..8f38f752cb 100644 --- a/tests/resources/functions/php-cookie/index.php +++ b/tests/resources/functions/php-cookie/index.php @@ -1,6 +1,5 @@ log($context->req->headers); return $context->res->send($context->req->headers['cookie'] ?? ''); }; From 71807635b369dbc11242e161c869fe93852aaaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 27 Oct 2023 17:23:43 +0200 Subject: [PATCH 5/6] Move cookie-header logic to request --- app/controllers/general.php | 11 +--------- src/Appwrite/Utopia/Request.php | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 14b074d21d..1f4a46d810 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -117,16 +117,7 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $path .= '?' . $query; } - $swooleHeaders = $swooleRequest->header; - - $cookieHeaders = []; - foreach ($swooleRequest->cookie as $key => $value) { - $cookieHeaders[] = "{$key}={$value}"; - } - - if (!empty($cookieHeaders)) { - $swooleHeaders['cookie'] = \implode('; ', $cookieHeaders); - } + $swooleHeaders = $request->getHeaders(); $body = \json_encode([ 'async' => false, diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index 229c9dd53d..a9eef62e06 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -95,4 +95,42 @@ class Request extends UtopiaRequest { return self::$route != null; } + + /** + * Get headers + * + * Method for getting all HTTP header parameters, including cookies. + * + * @return array + */ + public function getHeaders(): array + { + $headers = $this->generateHeaders(); + + $cookieHeaders = []; + foreach ($this->swoole->cookie as $key => $value) { + $cookieHeaders[] = "{$key}={$value}"; + } + + if (!empty($cookieHeaders)) { + $headers['cookie'] = \implode('; ', $cookieHeaders); + } + + return $headers; + } + + /** + * Get header + * + * Method for querying HTTP header parameters. If $key is not found $default value will be returned. + * + * @param string $key + * @param string $default + * @return string + */ + public function getHeader(string $key, string $default = ''): string + { + $headers = $this->getHeaders(); + return $headers[$key] ?? $default; + } } From 9a6d5f810174d9dd3f52d82c42274107e86d5c32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matej=20Ba=C4=8Do?= Date: Fri, 27 Oct 2023 17:25:19 +0200 Subject: [PATCH 6/6] Improve var name --- app/controllers/general.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/controllers/general.php b/app/controllers/general.php index 1f4a46d810..247669731a 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -117,14 +117,14 @@ function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleReques $path .= '?' . $query; } - $swooleHeaders = $request->getHeaders(); + $requestHeaders = $request->getHeaders(); $body = \json_encode([ 'async' => false, 'body' => $swooleRequest->getContent() ?? '', 'method' => $swooleRequest->server['request_method'], 'path' => $path, - 'headers' => $swooleHeaders + 'headers' => $requestHeaders ]); $headers = [ @@ -408,7 +408,7 @@ App::init() * @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers */ if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS - if ($request->getProtocol() !== 'https' && ($swooleHeaders['host'] ?? '') !== 'localhost' && ($swooleHeaders['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations + if ($request->getProtocol() !== 'https' && ($requestHeaders['host'] ?? '') !== 'localhost' && ($requestHeaders['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations if ($request->getMethod() !== Request::METHOD_GET) { throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.'); }