diff --git a/.env b/.env index 55ec662f24..3a0c4a50cb 100644 --- a/.env +++ b/.env @@ -125,3 +125,4 @@ _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10 _APP_PROJECT_REGIONS=default _APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000 _APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main +_APP_TRUSTED_HEADERS=x-forwarded-for \ No newline at end of file diff --git a/app/config/variables.php b/app/config/variables.php index 408be8d54e..653e959101 100644 --- a/app/config/variables.php +++ b/app/config/variables.php @@ -357,6 +357,15 @@ return [ 'required' => false, 'question' => '', 'filter' => '' + ], + [ + 'name' => '_APP_TRUSTED_HEADERS', + 'description' => 'This option allows you to set the list of trusted headers, the value is a comma‑separated list of HTTP header names, evaluated left-to-right for the first valid IP. Header names are treated case-insensitively.', + 'introduction' => '1.8.0', + 'default' => 'x-forwarded-for', + 'required' => false, + 'question' => '', + 'filter' => '' ] ], ], diff --git a/app/controllers/shared/api.php b/app/controllers/shared/api.php index 8467468ed6..83b56f626a 100644 --- a/app/controllers/shared/api.php +++ b/app/controllers/shared/api.php @@ -628,7 +628,6 @@ App::init() $queueForFunctions->setPlatform($platform); $queueForBuilds->setPlatform($platform); $queueForMails->setPlatform($platform); - $queueForMigrations->setPlatform($platform); // Clone the queues, to prevent events triggered by the database listener // from overwriting the events that are supposed to be triggered in the shutdown hook. diff --git a/composer.lock b/composer.lock index 47a32cf774..6277c96145 100644 --- a/composer.lock +++ b/composer.lock @@ -4264,16 +4264,16 @@ }, { "name": "utopia-php/framework", - "version": "0.33.34", + "version": "0.33.35", "source": { "type": "git", "url": "https://github.com/utopia-php/http.git", - "reference": "76def92594c32504ec80eaacdb60ff8fad73c856" + "reference": "82b139fb04f30045db51b0d322224f222da32313" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/http/zipball/76def92594c32504ec80eaacdb60ff8fad73c856", - "reference": "76def92594c32504ec80eaacdb60ff8fad73c856", + "url": "https://api.github.com/repos/utopia-php/http/zipball/82b139fb04f30045db51b0d322224f222da32313", + "reference": "82b139fb04f30045db51b0d322224f222da32313", "shasum": "" }, "require": { @@ -4306,9 +4306,9 @@ ], "support": { "issues": "https://github.com/utopia-php/http/issues", - "source": "https://github.com/utopia-php/http/tree/0.33.34" + "source": "https://github.com/utopia-php/http/tree/0.33.35" }, - "time": "2025-12-08T07:55:31+00:00" + "time": "2025-12-12T08:33:52+00:00" }, { "name": "utopia-php/image", @@ -5011,22 +5011,22 @@ }, { "name": "utopia-php/swoole", - "version": "0.8.4", + "version": "0.8.5", "source": { "type": "git", "url": "https://github.com/utopia-php/swoole.git", - "reference": "150c30700e738c52348cce9ed0e0f0ff96872081" + "reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/swoole/zipball/150c30700e738c52348cce9ed0e0f0ff96872081", - "reference": "150c30700e738c52348cce9ed0e0f0ff96872081", + "url": "https://api.github.com/repos/utopia-php/swoole/zipball/e42b6b8e44c457a7b35d8a857d7af1d67d667c58", + "reference": "e42b6b8e44c457a7b35d8a857d7af1d67d667c58", "shasum": "" }, "require": { "ext-swoole": "*", "php": ">=8.0", - "utopia-php/framework": "0.33.*" + "utopia-php/framework": "0.33.35" }, "require-dev": { "laravel/pint": "1.2.*", @@ -5056,9 +5056,9 @@ ], "support": { "issues": "https://github.com/utopia-php/swoole/issues", - "source": "https://github.com/utopia-php/swoole/tree/0.8.4" + "source": "https://github.com/utopia-php/swoole/tree/0.8.5" }, - "time": "2025-09-07T09:39:46+00:00" + "time": "2025-12-15T14:03:23+00:00" }, { "name": "utopia-php/system", @@ -8943,7 +8943,7 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { @@ -8967,5 +8967,5 @@ "platform-overrides": { "php": "8.3" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/docker-compose.yml b/docker-compose.yml index 87b45d7e4f..f18bb747b8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -220,6 +220,7 @@ services: - _APP_DATABASE_SHARED_NAMESPACE - _APP_FUNCTIONS_CREATION_ABUSE_LIMIT - _APP_CUSTOM_DOMAIN_DENY_LIST + - _APP_TRUSTED_HEADERS extra_hosts: - "host.docker.internal:host-gateway" @@ -736,6 +737,7 @@ services: - _APP_MIGRATIONS_FIREBASE_CLIENT_ID - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET - _APP_DATABASE_SHARED_TABLES + - _APP_OPTIONS_FORCE_HTTPS appwrite-task-maintenance: entrypoint: maintenance diff --git a/src/Appwrite/Event/Build.php b/src/Appwrite/Event/Build.php index 79437c3e58..38f423829e 100644 --- a/src/Appwrite/Event/Build.php +++ b/src/Appwrite/Event/Build.php @@ -2,6 +2,7 @@ namespace Appwrite\Event; +use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Queue\Publisher; @@ -110,13 +111,18 @@ class Build extends Event */ protected function preparePayload(): array { + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + return [ 'project' => $this->project, 'resource' => $this->resource, 'deployment' => $this->deployment, 'type' => $this->type, 'template' => $this->template, - 'platform' => $this->platform + 'platform' => $platform, ]; } diff --git a/src/Appwrite/Event/Database.php b/src/Appwrite/Event/Database.php index 8e7f6b7625..eac30ac07e 100644 --- a/src/Appwrite/Event/Database.php +++ b/src/Appwrite/Event/Database.php @@ -161,7 +161,7 @@ class Database extends Event return $this->document; } - public function setProject(Document $project): self + public function setProject(Document $project): static { $database = $project->getAttribute('database'); if (!empty($database)) { diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index f8fb012075..c7bb22f715 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -92,9 +92,9 @@ class Event * Set queue used for this event. * * @param string $queue - * @return Event + * @return static */ - public function setQueue(string $queue): self + public function setQueue(string $queue): static { $this->queue = $queue; @@ -114,9 +114,9 @@ class Event /** * Set event name used for this event. * @param string $event - * @return Event + * @return static */ - public function setEvent(string $event): self + public function setEvent(string $event): static { $this->event = $event; @@ -137,9 +137,9 @@ class Event * Set project for this event. * * @param Document $project - * @return self + * @return static */ - public function setProject(Document $project): self + public function setProject(Document $project): static { $this->project = $project; return $this; @@ -159,9 +159,9 @@ class Event * Set platform for this event. * * @param array $platform - * @return self + * @return static */ - public function setPlatform(array $platform): self + public function setPlatform(array $platform): static { $this->platform = $platform; return $this; @@ -181,9 +181,9 @@ class Event * Set user for this event. * * @param Document $user - * @return self + * @return static */ - public function setUser(Document $user): self + public function setUser(Document $user): static { $this->user = $user; @@ -193,9 +193,9 @@ class Event /** * Set user ID for this event. * - * @return self + * @return static */ - public function setUserId(string $userId): self + public function setUserId(string $userId): static { $this->userId = $userId; @@ -225,9 +225,9 @@ class Event * * @param array $payload * @param array $sensitive - * @return self + * @return static */ - public function setPayload(array $payload, array $sensitive = []): self + public function setPayload(array $payload, array $sensitive = []): static { $this->payload = $payload; diff --git a/src/Appwrite/Event/Func.php b/src/Appwrite/Event/Func.php index 380a28f1db..c9622a9ce0 100644 --- a/src/Appwrite/Event/Func.php +++ b/src/Appwrite/Event/Func.php @@ -2,6 +2,7 @@ namespace Appwrite\Event; +use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Queue\Publisher; @@ -202,6 +203,11 @@ class Func extends Event { $events = $this->getEvent() ? Event::generateEvents($this->getEvent(), $this->getParams()) : null; + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + return [ 'project' => $this->project, 'user' => $this->user, @@ -217,7 +223,7 @@ class Func extends Event 'path' => $this->path, 'headers' => $this->headers, 'method' => $this->method, - 'platform' => $this->platform + 'platform' => $platform, ]; } } diff --git a/src/Appwrite/Event/Mail.php b/src/Appwrite/Event/Mail.php index c801d30493..ed96f6f93a 100644 --- a/src/Appwrite/Event/Mail.php +++ b/src/Appwrite/Event/Mail.php @@ -2,6 +2,7 @@ namespace Appwrite\Event; +use Utopia\Config\Config; use Utopia\Queue\Publisher; class Mail extends Event @@ -516,6 +517,11 @@ class Mail extends Event */ protected function preparePayload(): array { + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + return [ 'project' => $this->project, 'recipient' => $this->recipient, @@ -528,7 +534,8 @@ class Mail extends Event 'variables' => $this->variables, 'attachment' => $this->attachment, 'customMailOptions' => $this->customMailOptions, - 'events' => Event::generateEvents($this->getEvent(), $this->getParams()) + 'events' => Event::generateEvents($this->getEvent(), $this->getParams()), + 'platform' => $platform, ]; } } diff --git a/src/Appwrite/Event/Messaging.php b/src/Appwrite/Event/Messaging.php index 3ddbac1040..f4c72c7d72 100644 --- a/src/Appwrite/Event/Messaging.php +++ b/src/Appwrite/Event/Messaging.php @@ -161,19 +161,6 @@ class Messaging extends Event return $this->scheduledAt; } - /** - * Set project for this event. - * - * @param Document $project - * @return self - */ - public function setProject(Document $project): self - { - $this->project = $project; - - return $this; - } - /** * Prepare the payload for the event * diff --git a/src/Appwrite/Event/Migration.php b/src/Appwrite/Event/Migration.php index ca54310ce6..a224d4f4c3 100644 --- a/src/Appwrite/Event/Migration.php +++ b/src/Appwrite/Event/Migration.php @@ -2,6 +2,7 @@ namespace Appwrite\Event; +use Utopia\Config\Config; use Utopia\Database\Document; use Utopia\Queue\Publisher; @@ -73,11 +74,16 @@ class Migration extends Event */ protected function preparePayload(): array { + $platform = $this->platform; + if (empty($platform)) { + $platform = Config::getParam('platform', []); + } + return [ 'project' => $this->project, 'user' => $this->user, 'migration' => $this->migration, - 'platform' => $this->platform, + 'platform' => $platform, ]; } } diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php index 38e7f8f231..a3a1ea6ce8 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Decrement.php @@ -177,6 +177,7 @@ class Decrement extends Action value: $value, min: $min ); + $document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collectionId); } catch (ConflictException) { throw new Exception($this->getConflictException()); } catch (NotFoundException) { diff --git a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php index 10dadae824..157c5ef2af 100644 --- a/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php +++ b/src/Appwrite/Platform/Modules/Databases/Http/Databases/Collections/Documents/Attribute/Increment.php @@ -177,6 +177,7 @@ class Increment extends Action value: $value, max: $max ); + $document->setAttribute('$' . $this->getCollectionsEventsContext() . 'Id', $collectionId); } catch (ConflictException) { throw new Exception($this->getConflictException()); } catch (NotFoundException) { diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 64224a9770..ae94c7580d 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -62,9 +62,6 @@ class Messaging extends Action */ public function __construct() { - - $this->adapter = $this->createInternalSMSAdapter(); - $this ->desc('Messaging worker') ->inject('message') @@ -390,6 +387,10 @@ class Messaging extends Action private function sendInternalSMSMessage(Document $message, Document $project, array $recipients, Log $log): void { + if ($this->adapter === null) { + $this->adapter = $this->createInternalSMSAdapter(); + } + if ($this->adapter === null) { Console::warning('Skipped SMS processing. SMS adapter is not set.'); return; diff --git a/src/Appwrite/Platform/Workers/Migrations.php b/src/Appwrite/Platform/Workers/Migrations.php index 779831c7d6..78ae7c4ced 100644 --- a/src/Appwrite/Platform/Workers/Migrations.php +++ b/src/Appwrite/Platform/Workers/Migrations.php @@ -47,7 +47,6 @@ class Migrations extends Action protected ?Device $deviceForFiles; protected ?Document $project; protected array $plan = []; - protected array $platform = []; /** * @var array @@ -103,7 +102,6 @@ class Migrations extends Action $this->deviceForMigrations = $deviceForMigrations; $this->deviceForFiles = $deviceForFiles; $this->plan = $plan; - $this->platform = $payload['platform'] ?? []; if (empty($payload)) { throw new Exception('Missing payload'); @@ -135,7 +133,6 @@ class Migrations extends Action $this->deviceForMigrations = null; $this->deviceForFiles = null; $this->plan = []; - $this->platform = []; $this->sourceReport = []; gc_collect_cycles(); @@ -153,10 +150,15 @@ class Migrations extends Action $credentials = $migration->getAttribute('credentials'); $migrationOptions = $migration->getAttribute('options'); $dataSource = Appwrite::SOURCE_API; - $endpoint = $this->platform['endpoint'] ?: ($credentials['endpoint'] ?? 'http://appwrite.test/v1'); $database = null; $queries = []; + if ($credentials['endpoint'] === 'http://localhost/v1') { + $platform = Config::getParam('platform', []); + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; + $credentials['endpoint'] = $protocol . '://' . $platform['apiHostname'] . '/v1'; + } + if ($source === Appwrite::getName() && $destination === DestinationCSV::getName()) { $dataSource = Appwrite::SOURCE_DATABASE; $database = $this->dbForProject; @@ -187,7 +189,7 @@ class Migrations extends Action ), SourceAppwrite::getName() => new SourceAppwrite( $credentials['projectId'], - $endpoint, + $credentials['endpoint'], $credentials['apiKey'], $dataSource, $database, @@ -215,10 +217,13 @@ class Migrations extends Action $destination = $migration->getAttribute('destination'); $options = $migration->getAttribute('options', []); + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; + $platform = Config::getParam('platform', []); + return match ($destination) { DestinationAppwrite::getName() => new DestinationAppwrite( $this->project->getId(), - $this->platform['endpoint'], + $protocol . '://' . $platform['apiHostname'] . '/v1', $apiKey, $this->dbForProject, Config::getParam('collections', [])['databases']['collections'], @@ -322,8 +327,13 @@ class Migrations extends Action ) { $credentials = $migration->getAttribute('credentials', []); $credentials['projectId'] = $credentials['projectId'] ?? $project->getId(); - $credentials['endpoint'] = $credentials['endpoint'] ?? $this->platform['endpoint']; $credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey; + + if (empty($credentials['endpoint'])) { + $platform = Config::getParam('platform', []); + $protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https'; + $credentials['endpoint'] = $protocol . '://' . $platform['apiHostname'] . '/v1'; + } $migration->setAttribute('credentials', $credentials); } diff --git a/src/Appwrite/Utopia/Request.php b/src/Appwrite/Utopia/Request.php index ce570d2af9..cb449e6ffa 100644 --- a/src/Appwrite/Utopia/Request.php +++ b/src/Appwrite/Utopia/Request.php @@ -9,6 +9,7 @@ use Swoole\Http\Request as SwooleRequest; use Utopia\Database\Validator\Authorization; use Utopia\Route; use Utopia\Swoole\Request as UtopiaRequest; +use Utopia\System\System; class Request extends UtopiaRequest { @@ -20,6 +21,9 @@ class Request extends UtopiaRequest public function __construct(SwooleRequest $request) { + $trustedHeaders = System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for'); + $this->setTrustedIpHeaders(explode(',', $trustedHeaders)); + parent::__construct($request); } diff --git a/tests/e2e/Services/Account/AccountBase.php b/tests/e2e/Services/Account/AccountBase.php index 13b5015241..0c9d481371 100644 --- a/tests/e2e/Services/Account/AccountBase.php +++ b/tests/e2e/Services/Account/AccountBase.php @@ -326,4 +326,41 @@ trait AccountBase $this->assertEquals($response['headers']['status-code'], 204); } + + public function testFallbackForTrustedIp(): void + { + $email = uniqid() . 'user@localhost.test'; + $password = 'password'; + $name = 'User Name'; + + // call appwrite directly to avoid proxy stripping the headers + $this->client->setEndpoint('http://localhost/v1'); + + $response = $this->client->call(Client::METHOD_POST, '/account', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-forwarded-for' => '191.0.113.195', + ]), [ + 'userId' => ID::unique(), + 'email' => $email, + 'password' => $password, + 'name' => $name, + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + + $response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([ + 'origin' => 'http://localhost', + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-forwarded-for' => '191.0.113.195', + ]), [ + 'email' => $email, + 'password' => $password, + ]); + + $this->assertEquals($response['headers']['status-code'], 201); + $this->assertEquals('191.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? ''); + } } diff --git a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php index d43d183e5a..d1d2c9687d 100644 --- a/tests/e2e/Services/Databases/Legacy/DatabasesBase.php +++ b/tests/e2e/Services/Databases/Legacy/DatabasesBase.php @@ -6118,6 +6118,7 @@ trait DatabasesBase ])); $this->assertEquals(200, $inc['headers']['status-code']); $this->assertEquals(6, $inc['body']['count']); + $this->assertEquals($collectionId, $inc['body']['$collectionId']); // Verify count = 6 $get = $this->client->call(Client::METHOD_GET, "/databases/$databaseId/collections/$collectionId/documents/$docId", array_merge([ @@ -6229,6 +6230,7 @@ trait DatabasesBase ])); $this->assertEquals(200, $dec['headers']['status-code']); $this->assertEquals(9, $dec['body']['count']); + $this->assertEquals($collectionId, $dec['body']['$collectionId']); $get = $this->client->call(Client::METHOD_GET, '/databases/' . $databaseId . '/collections/' . $collectionId . '/documents/' . $documentId, array_merge([ 'content-type' => 'application/json', diff --git a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php index e74431c779..ba111e5923 100644 --- a/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php +++ b/tests/e2e/Services/Databases/TablesDB/DatabasesBase.php @@ -7760,6 +7760,7 @@ trait DatabasesBase 'x-appwrite-project' => $this->getProject()['$id'], ])); $this->assertEquals(200, $inc['headers']['status-code']); + $this->assertEquals($tableId, $inc['body']['$tableId']); $this->assertEquals(6, $inc['body']['count']); // Verify count = 6 @@ -7872,6 +7873,7 @@ trait DatabasesBase ])); $this->assertEquals(200, $dec['headers']['status-code']); $this->assertEquals(9, $dec['body']['count']); + $this->assertEquals($tableId, $dec['body']['$tableId']); $get = $this->client->call(Client::METHOD_GET, '/tablesdb/' . $databaseId . '/tables/' . $tableId . '/rows/' . $rowId, array_merge([ 'content-type' => 'application/json',