diff --git a/.env b/.env index ea666a2ac..2dc49cbe7 100644 --- a/.env +++ b/.env @@ -14,6 +14,7 @@ _APP_OPTIONS_ABUSE=disabled _APP_OPTIONS_FORCE_HTTPS=disabled _APP_OPENSSL_KEY_V1=your-secret-key _APP_DOMAIN=demo.appwrite.io +_APP_DOMAIN_FUNCTIONS=functions.localhost _APP_DOMAIN_TARGET=demo.appwrite.io _APP_REDIS_HOST=redis _APP_REDIS_PORT=6379 diff --git a/Dockerfile b/Dockerfile index 54aa32776..ba17aea76 100755 --- a/Dockerfile +++ b/Dockerfile @@ -191,6 +191,7 @@ ENV _APP_SERVER=swoole \ _APP_LOCALE=en \ _APP_DOMAIN=localhost \ _APP_DOMAIN_TARGET=localhost \ + _APP_DOMAIN_FUNCTIONS=disabled \ _APP_HOME=https://appwrite.io \ _APP_EDITION=community \ _APP_CONSOLE_WHITELIST_ROOT=enabled \ diff --git a/app/config/errors.php b/app/config/errors.php index a071b0cb7..a56a9f8a0 100644 --- a/app/config/errors.php +++ b/app/config/errors.php @@ -514,6 +514,16 @@ return [ 'description' => 'The project key has expired. Please generate a new key using the Appwrite console.', 'code' => 401, ], + Exception::ROUTER_INVALID_URL => [ + 'name' => Exception::ROUTER_INVALID_URL, + 'description' => 'Invalid preview URL for Appwrite Router.', + 'code' => 400, + ], + Exception::ROUTER_CONSOLE_PROJECT => [ + 'name' => Exception::ROUTER_CONSOLE_PROJECT, + 'description' => 'Use of "console" project with Appwrite Router is not allowed.', + 'code' => 400, + ], Exception::WEBHOOK_NOT_FOUND => [ 'name' => Exception::WEBHOOK_NOT_FOUND, 'description' => 'Webhook with the requested ID could not be found.', diff --git a/app/config/variables.php b/app/config/variables.php index 85c7a04f2..46abf671c 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -61,6 +61,15 @@ return [ 'question' => 'Enter your Appwrite hostname', 'filter' => '' ], + [ + 'name' => '_APP_DOMAIN_FUNCTIONS', + 'description' => 'A domain to use for function preview URLs. Setting to "disabled" turns off function preview URLs.', + 'introduction' => '', + 'default' => 'disabled', + 'required' => false, + 'question' => '', + 'filter' => '' + ], [ 'name' => '_APP_DOMAIN_TARGET', 'description' => 'A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite \'_APP_DOMAIN\' variable. The default value is \'localhost\'.', diff --git a/app/controllers/general.php b/app/controllers/general.php index 0c10dd46e..4988e6365 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -8,6 +8,10 @@ use Utopia\Locale\Locale; use Utopia\Logger\Logger; use Utopia\Logger\Log; use Utopia\Logger\Log\User; +use Swoole\Http\Request as SwooleRequest; +use Swoole\Http\Response as SwooleResponse; +use Utopia\Cache\Cache; +use Utopia\Pools\Group; use Appwrite\Utopia\Request; use Appwrite\Utopia\Response; use Appwrite\Utopia\View; @@ -42,6 +46,8 @@ Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE); App::init() ->inject('utopia') + ->inject('swooleRequest') + ->inject('swooleResponse') ->inject('request') ->inject('response') ->inject('console') @@ -51,7 +57,80 @@ App::init() ->inject('locale') ->inject('clients') ->inject('servers') - ->action(function (App $utopia, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients, array $servers) { + ->inject('pools') + ->inject('cache') + ->action(function (App $utopia, SwooleRequest $swooleRequest, SwooleResponse $swooleResponse, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $clients, array $servers, Group $pools, Cache $cache) { + /* + * Appwrite Router + */ + $host = $swooleRequest->header['host'] ?? ''; + + // Function Preview + if(\str_ends_with($host, App::getEnv('_APP_DOMAIN_FUNCTIONS'))) { + $host = rtrim($host, App::getEnv('_APP_DOMAIN_FUNCTIONS')); + $host = rtrim($host, '.'); + $subdomains = explode('.', $host); + + if(\count($subdomains) === 2) { + // Active deployment preview + $functionId = $subdomains[0]; + $projectId = $subdomains[1]; + + if($projectId === 'console') { + throw new AppwriteException(AppwriteException::ROUTER_CONSOLE_PROJECT); + } + + // TODO: Rate limit? + + $body = \json_encode([ + 'async' => false, + 'data' => 'Heyy' + ]); + + $headers = [ + 'Content-Type: application/json', + 'Content-Length: ' . \strlen($body), + 'X-Appwrite-Project: ' . $projectId + ]; + + $ch = \curl_init(); + \curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions"); + \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST"); + \curl_setopt($ch, CURLOPT_POSTFIELDS, $body); + \curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + // \curl_setopt($ch, CURLOPT_HEADER, true); + \curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); + + $executionResponse = \utf8_decode(\curl_exec($ch)); + $statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE); + $error = \curl_error($ch); + $errNo = \curl_errno($ch); + + \curl_close($ch); + + $responseBody = \json_decode($executionResponse, true)['response']; + + return $response->setStatusCode(200)->send($responseBody); + } else if(\count($subdomains) === 3) { + // Deployment preview + $deploymentId = $subdomains[0]; + $functionId = $subdomains[1]; + $projectId = $subdomains[2]; + + if($projectId === 'console') { + throw new AppwriteException(AppwriteException::ROUTER_CONSOLE_PROJECT); + } + + // TODO: Implement + throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Deployment preview not implemented yet.'); + } else { + throw new AppwriteException(AppwriteException::ROUTER_INVALID_URL); + } + } + + // TODO: Custom domains + /* * Request format */ diff --git a/app/http.php b/app/http.php index 23fd0dbcc..892f6791d 100644 --- a/app/http.php +++ b/app/http.php @@ -229,6 +229,9 @@ $http->on('start', function (Server $http) use ($payloadSize, $register) { }); $http->on('request', function (SwooleRequest $swooleRequest, SwooleResponse $swooleResponse) use ($register) { + App::setResource('swooleRequest', fn() => $swooleRequest); + App::setResource('swooleResponse', fn() => $swooleResponse); + $request = new Request($swooleRequest); $response = new Response($swooleResponse); diff --git a/app/views/install/compose.phtml b/app/views/install/compose.phtml index cc53fb885..3be927a65 100644 --- a/app/views/install/compose.phtml +++ b/app/views/install/compose.phtml @@ -98,6 +98,7 @@ services: - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS - _APP_DB_HOST - _APP_DB_PORT - _APP_DB_SCHEMA @@ -345,6 +346,7 @@ services: - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS - _APP_DB_HOST - _APP_DB_PORT @@ -449,6 +451,7 @@ services: - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS - _APP_DB_HOST - _APP_DB_PORT - _APP_DB_SCHEMA diff --git a/docker-compose.yml b/docker-compose.yml index d09e5649a..8b5b55fc4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -114,6 +114,7 @@ services: - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS - _APP_DB_HOST - _APP_DB_PORT - _APP_DB_SCHEMA @@ -424,6 +425,7 @@ services: - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS - _APP_DB_HOST - _APP_DB_PORT @@ -562,6 +564,7 @@ services: - _APP_POOL_CLIENTS - _APP_DOMAIN - _APP_DOMAIN_TARGET + - _APP_DOMAIN_FUNCTIONS - _APP_OPENSSL_KEY_V1 - _APP_DB_HOST - _APP_DB_PORT diff --git a/src/Appwrite/Extend/Exception.php b/src/Appwrite/Extend/Exception.php index 9f035863e..e6a186758 100644 --- a/src/Appwrite/Extend/Exception.php +++ b/src/Appwrite/Extend/Exception.php @@ -163,6 +163,10 @@ class Exception extends \Exception /** Webhooks */ public const WEBHOOK_NOT_FOUND = 'webhook_not_found'; + /** Router */ + public const ROUTER_INVALID_URL = 'router_invalid_url'; + public const ROUTER_CONSOLE_PROJECT = 'router_console_project'; + /** Keys */ public const KEY_NOT_FOUND = 'key_not_found'; diff --git a/tests/resources/docker/docker-compose.yml b/tests/resources/docker/docker-compose.yml index e7bbaf270..810ead6cf 100644 --- a/tests/resources/docker/docker-compose.yml +++ b/tests/resources/docker/docker-compose.yml @@ -70,6 +70,7 @@ services: - _APP_OPTIONS_FORCE_HTTPS - _APP_OPENSSL_KEY_V1 - _APP_DOMAIN + - _APP_DOMAIN_FUNCTIONS - _APP_DOMAIN_TARGET - _APP_REDIS_HOST - _APP_REDIS_PORT