Merge pull request #10911 from appwrite/feat-multiple-app-domains

feat: multiple app domains
This commit is contained in:
Luke B. Silver 2025-12-11 10:09:33 +00:00 committed by GitHub
commit 61d1546d14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
64 changed files with 943 additions and 663 deletions

4
.env
View file

@ -22,7 +22,7 @@ _APP_OPTIONS_FORCE_HTTPS=disabled
_APP_OPTIONS_ROUTER_FORCE_HTTPS=disabled
_APP_OPENSSL_KEY_V1=your-secret-key
_APP_DNS=8.8.8.8
_APP_DOMAIN=traefik
_APP_DOMAIN=appwrite.test
_APP_CONSOLE_DOMAIN=localhost
_APP_DOMAIN_FUNCTIONS=functions.localhost
_APP_DOMAIN_SITES=sites.localhost
@ -124,4 +124,4 @@ _APP_MESSAGE_PUSH_TEST_DSN=
_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_STATS_USAGE_DUAL_WRITING_DBS=database_db_main

21
app/config/platform.php Normal file
View file

@ -0,0 +1,21 @@
<?php
use Utopia\System\System;
/**
* Platform configuration
*/
return [
'domain' => System::getEnv('_APP_DOMAIN', 'localhost'),
'consoleDomain' => System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', 'localhost')),
'platformName' => APP_EMAIL_PLATFORM_NAME,
'logoUrl' => APP_EMAIL_LOGO_URL,
'accentColor' => APP_EMAIL_ACCENT_COLOR,
'footerImageUrl' => APP_EMAIL_FOOTER_IMAGE_URL,
'twitterUrl' => APP_SOCIAL_TWITTER,
'discordUrl' => APP_SOCIAL_DISCORD,
'githubUrl' => APP_SOCIAL_GITHUB,
'termsUrl' => APP_EMAIL_TERMS_URL,
'privacyUrl' => APP_EMAIL_PRIVACY_URL,
'websiteUrl' => 'https://' . APP_DOMAIN,
];

View file

@ -1,5 +1,6 @@
<?php
use Utopia\Config\Config;
use Utopia\System\System;
/**
@ -7,12 +8,8 @@ use Utopia\System\System;
*/
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN', '');
// Temporary fix until we can set _APP_DOMAIN to "localhost" instead of "traefik"
if (System::getEnv('_APP_ENV', 'development') === 'development') {
$hostname = 'localhost';
}
$platform = Config::getParam('platform', []);
$hostname = $platform['consoleDomain'] ?? '';
$url = $protocol . '://' . $hostname;

View file

@ -81,7 +81,7 @@ return [
],
[
'name' => '_APP_DOMAIN',
'description' => 'Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is \'localhost\'.',
'description' => 'Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is \'localhost\'. Multiple domains can be separated by commas.',
'introduction' => '',
'default' => 'localhost',
'required' => true,

View file

@ -64,11 +64,11 @@ use Utopia\Emails\Email;
use Utopia\Locale\Locale;
use Utopia\Storage\Validator\FileName;
use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
$oauthDefaultSuccess = '/console/auth/oauth2/success';
@ -1283,13 +1283,14 @@ App::get('/v1/account/sessions/oauth2/:provider')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('success', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('failure', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('request')
->inject('response')
->inject('project')
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
->inject('platform')
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project, array $platform) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@ -1324,7 +1325,7 @@ App::get('/v1/account/sessions/oauth2/:provider')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$host = $platform['consoleDomain'] ?? '';
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
@ -1443,7 +1444,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('request')
->inject('response')
->inject('project')
->inject('platforms')
->inject('redirectValidator')
->inject('devKey')
->inject('user')
->inject('dbForProject')
@ -1452,7 +1453,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
->inject('store')
->inject('proofForPassword')
->inject('proofForToken')
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, array $platforms, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
->action(function (string $provider, string $code, string $state, string $error, string $error_description, Request $request, Response $response, Document $project, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
@ -1463,7 +1464,6 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
}
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$defaultState = ['success' => $project->getAttribute('url', ''), 'failure' => ''];
$redirect = new Redirect($platforms);
$appId = $project->getAttribute('oAuthProviders', [])[$provider . 'Appid'] ?? '';
$appSecret = $project->getAttribute('oAuthProviders', [])[$provider . 'Secret'] ?? '{}';
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
@ -1490,11 +1490,11 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
$state = $defaultState;
}
if ($devKey->isEmpty() && !$redirect->isValid($state['success'])) {
if ($devKey->isEmpty() && !$redirectValidator->isValid($state['success'])) {
throw new Exception(Exception::PROJECT_INVALID_SUCCESS_URL);
}
if ($devKey->isEmpty() && !empty($state['failure']) && !$redirect->isValid($state['failure'])) {
if ($devKey->isEmpty() && !empty($state['failure']) && !$redirectValidator->isValid($state['failure'])) {
throw new Exception(Exception::PROJECT_INVALID_FAILURE_URL);
}
$failure = [];
@ -1945,23 +1945,15 @@ App::get('/v1/account/tokens/oauth2/:provider')
->label('abuse-limit', 50)
->label('abuse-key', 'ip:{ip}')
->param('provider', '', new WhiteList(\array_keys(Config::getParam('oAuthProviders')), true), 'OAuth2 Provider. Currently, supported providers are: ' . \implode(', ', \array_keys(\array_filter(Config::getParam('oAuthProviders'), fn ($node) => (!$node['mock'])))) . '.')
->param('success', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('failure', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('scopes', [], new ArrayList(new Text(APP_LIMIT_ARRAY_ELEMENT_SIZE), APP_LIMIT_ARRAY_PARAMS_SIZE), 'A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' scopes are allowed, each ' . APP_LIMIT_ARRAY_ELEMENT_SIZE . ' characters long.', true)
->inject('request')
->inject('response')
->inject('project')
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $request->getHostname();
if ($protocol === 'https' && $port !== '443') {
$callbackBase .= ':' . $port;
} elseif ($protocol === 'http' && $port !== '80') {
$callbackBase .= ':' . $port;
}
$callback = $callbackBase . '/v1/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
->inject('platform')
->action(function (string $provider, string $success, string $failure, array $scopes, Request $request, Response $response, Document $project, array $platform) use ($oauthDefaultSuccess, $oauthDefaultFailure) {
$callback = $platform['endpoint'] . '/account/sessions/oauth2/callback/' . $provider . '/' . $project->getId();
$providerEnabled = $project->getAttribute('oAuthProviders', [])[$provider . 'Enabled'] ?? false;
if (!$providerEnabled) {
@ -1986,7 +1978,9 @@ App::get('/v1/account/tokens/oauth2/:provider')
throw new Exception(Exception::PROJECT_PROVIDER_UNSUPPORTED);
}
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$host = $platform['consoleDomain'] ?? '';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = $request->getPort();
$redirectBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
$redirectBase .= ':' . $port;
@ -2041,7 +2035,7 @@ App::post('/v1/account/tokens/magic-url')
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('userId', '', new CustomId(), 'Unique Id. Choose a custom ID or generate a random ID with `ID.unique()`. 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. If the email address has never been used, a new account is created using the provided userId. Otherwise, if the email address is already attached to an account, the user ID is ignored.')
->param('email', '', new EmailValidator(), 'User email.')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey'])
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the magic URL login. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('phrase', false, new Boolean(), 'Toggle for security phrase. If enabled, email will be send with a randomly generated phrase and the phrase will also be included in the response. Confirming phrases match increases the security of your authentication flow.', true)
->inject('request')
->inject('response')
@ -2052,7 +2046,8 @@ App::post('/v1/account/tokens/magic-url')
->inject('queueForEvents')
->inject('queueForMails')
->inject('proofForPassword')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword) {
->inject('platform')
->action(function (string $userId, string $email, string $url, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, array $platform) {
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
}
@ -2158,7 +2153,7 @@ App::post('/v1/account/tokens/magic-url')
if (empty($url)) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$host = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$host = $platform['consoleDomain'] ?? '';
$port = $request->getPort();
$callbackBase = $protocol . '://' . $host;
if ($protocol === 'https' && $port !== '443') {
@ -3463,7 +3458,7 @@ App::post('/v1/account/recovery')
->label('abuse-limit', 10)
->label('abuse-key', ['url:{url},email:{param-email}', 'url:{url},ip:{ip}'])
->param('email', '', new EmailValidator(), 'User email.')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey'])
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the recovery email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['redirectValidator'])
->inject('request')
->inject('response')
->inject('user')
@ -3759,7 +3754,7 @@ App::post('/v1/account/verifications/email')
])
->label('abuse-limit', 10)
->label('abuse-key', 'url:{url},userId:{userId}')
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['platforms', 'devKey']) // TODO add built-in confirm page
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the verification email. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', false, ['redirectValidator']) // TODO add built-in confirm page
->inject('request')
->inject('response')
->inject('project')

View file

@ -43,9 +43,6 @@ App::get('/v1/console/variables')
))
->inject('response')
->action(function (Response $response) {
$validator = new Domain(System::getEnv('_APP_DOMAIN'));
$isDomainValid = !empty(System::getEnv('_APP_DOMAIN', '')) && $validator->isKnown() && !$validator->isTest();
$validator = new Domain(System::getEnv('_APP_DOMAIN_TARGET_CNAME'));
$isCNAMEValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_CNAME', '')) && $validator->isKnown() && !$validator->isTest();
@ -55,9 +52,7 @@ App::get('/v1/console/variables')
$validator = new IP(IP::V6);
$isAAAAValid = !empty(System::getEnv('_APP_DOMAIN_TARGET_AAAA', '')) && $validator->isValid(System::getEnv('_APP_DOMAIN_TARGET_AAAA'));
$isDomainEnabled = $isDomainValid && (
$isAAAAValid || $isAValid || $isCNAMEValid
);
$isDomainEnabled = $isAAAAValid || $isAValid || $isCNAMEValid;
$isVcsEnabled = !empty(System::getEnv('_APP_VCS_GITHUB_APP_NAME', ''))
&& !empty(System::getEnv('_APP_VCS_GITHUB_PRIVATE_KEY', ''))

View file

@ -3495,7 +3495,8 @@ App::post('/v1/messaging/messages/push')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->inject('platform')
->action(function (string $messageId, string $title, string $body, ?array $topics, ?array $users, ?array $targets, ?array $data, string $action, string $image, string $icon, string $sound, string $color, string $tag, int $badge, bool $draft, ?string $scheduledAt, bool $contentAvailable, bool $critical, string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
$messageId = $messageId == 'unique()'
? ID::unique()
: $messageId;
@ -3551,7 +3552,6 @@ App::post('/v1/messaging/messages/push')
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$host = System::getEnv('_APP_DOMAIN', 'localhost');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
@ -3572,7 +3572,7 @@ App::post('/v1/messaging/messages/push')
$image = [
'bucketId' => $bucket->getId(),
'fileId' => $file->getId(),
'url' => "{$protocol}://{$host}/v1/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
'url' => "{$platform['endpoint']}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
];
}
@ -4378,7 +4378,8 @@ App::patch('/v1/messaging/messages/push/:messageId')
->inject('project')
->inject('queueForMessaging')
->inject('response')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response) {
->inject('platform')
->action(function (string $messageId, ?array $topics, ?array $users, ?array $targets, ?string $title, ?string $body, ?array $data, ?string $action, ?string $image, ?string $icon, ?string $sound, ?string $color, ?string $tag, ?int $badge, ?bool $draft, ?string $scheduledAt, ?bool $contentAvailable, ?bool $critical, ?string $priority, Event $queueForEvents, Database $dbForProject, Database $dbForPlatform, Document $project, Messaging $queueForMessaging, Response $response, array $platform) {
$message = $dbForProject->getDocument('messages', $messageId);
if ($message->isEmpty()) {
@ -4546,9 +4547,6 @@ App::patch('/v1/messaging/messages/push/:messageId')
throw new Exception(Exception::STORAGE_FILE_TYPE_UNSUPPORTED);
}
$host = System::getEnv('_APP_DOMAIN', 'localhost');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$scheduleTime = $currentScheduledAt ?? $scheduledAt;
if (!\is_null($scheduleTime)) {
$expiry = (new \DateTime($scheduleTime))->add(new \DateInterval('P15D'))->format('U');
@ -4567,7 +4565,7 @@ App::patch('/v1/messaging/messages/push/:messageId')
$pushData['image'] = [
'bucketId' => $bucket->getId(),
'fileId' => $file->getId(),
'url' => "{$protocol}://{$host}/v1/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}"
'url' => "{$platform['endpoint']}/storage/buckets/{$bucket->getId()}/files/{$file->getId()}/push?project={$project->getId()}&jwt={$jwt}",
];
}

View file

@ -58,7 +58,6 @@ use Utopia\Validator\ArrayList;
use Utopia\Validator\Assoc;
use Utopia\Validator\Boolean;
use Utopia\Validator\Text;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
App::post('/v1/teams')
@ -486,7 +485,7 @@ App::post('/v1/teams/:teamId/memberships')
}
return new ArrayList(new Key(), APP_LIMIT_ARRAY_PARAMS_SIZE);
}, 'Array of strings. Use this param to set the user roles in the team. A role can be any string. Learn more about [roles and permissions](https://appwrite.io/docs/permissions). Maximum of ' . APP_LIMIT_ARRAY_PARAMS_SIZE . ' roles are allowed, each 32 characters long.', false, ['project'])
->param('url', '', fn ($platforms, $devKey) => $devKey->isEmpty() ? new Redirect($platforms) : new URL(), 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['platforms', 'devKey']) // TODO add our own built-in confirm page
->param('url', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect the user back to your app from the invitation email. This parameter is not required when an API key is supplied. Only URLs from hostnames in your project platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) // TODO add our own built-in confirm page
->param('name', '', new Text(128), 'Name of the new team member. Max length: 128 chars.', true)
->inject('response')
->inject('project')

View file

@ -4,7 +4,6 @@ use Appwrite\Auth\OAuth2\Github as OAuth2Github;
use Appwrite\Event\Build;
use Appwrite\Event\Delete;
use Appwrite\Extend\Exception;
use Appwrite\Network\Validator\Redirect;
use Appwrite\SDK\AuthType;
use Appwrite\SDK\ContentType;
use Appwrite\SDK\Method;
@ -77,7 +76,7 @@ use Utopia\VCS\Exception\RepositoryNotFound;
use function Swoole\Coroutine\batch;
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, Request $request) {
$createGitDeployments = function (GitHub $github, string $providerInstallationId, array $repositories, string $providerBranch, string $providerBranchUrl, string $providerRepositoryName, string $providerRepositoryUrl, string $providerRepositoryOwner, string $providerCommitHash, string $providerCommitAuthor, string $providerCommitAuthorUrl, string $providerCommitMessage, string $providerCommitUrl, string $providerPullRequestId, bool $external, Database $dbForPlatform, Build $queueForBuilds, callable $getProjectDB, array $platform) {
$errors = [];
foreach ($repositories as $repository) {
try {
@ -133,7 +132,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$commentStatus = $isAuthorized ? 'waiting' : 'failed';
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $platform['consoleDomain'] ?? '';
$authorizeUrl = $protocol . '://' . $hostname . "/console/git/authorize-contributor?projectId={$projectId}&installationId={$installationId}&repositoryId={$repositoryId}&providerPullRequestId={$providerPullRequestId}";
@ -175,7 +174,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
@ -185,7 +184,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
}
}
} else {
$comment = new Comment();
$comment = new Comment($platform);
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
$latestCommentId = \strval($github->createComment($owner, $repositoryName, $providerPullRequestId, $comment->generateComment()));
@ -246,7 +245,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
if ($lockAcquired) {
// Wrap in try/finally to ensure lock file gets deleted
try {
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, '');
@ -467,7 +466,7 @@ $createGitDeployments = function (GitHub $github, string $providerInstallationId
$previewUrl = !empty($rule) ? ("{$protocol}://" . $rule->getAttribute('domain', '')) : '';
if (!empty($previewUrl)) {
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $latestCommentId));
$comment->addBuild($project, $resource, $resourceType, $commentStatus, $deploymentId, $action, $previewUrl);
$github->updateComment($owner, $repositoryName, $latestCommentId, $comment->generateComment());
@ -542,12 +541,12 @@ App::get('/v1/vcs/github/authorize')
type: MethodType::WEBAUTH,
hide: true,
))
->param('success', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a successful installation attempt.', true, ['platforms'])
->param('failure', '', fn ($platforms) => new Redirect($platforms), 'URL to redirect back to console after a failed installation attempt.', true, ['platforms'])
->inject('request')
->param('success', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a successful installation attempt.', true, ['redirectValidator'])
->param('failure', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to console after a failed installation attempt.', true, ['redirectValidator'])
->inject('response')
->inject('project')
->action(function (string $success, string $failure, Request $request, Response $response, Document $project) {
->inject('platform')
->action(function (string $success, string $failure, Response $response, Document $project, array $platform) {
$state = \json_encode([
'projectId' => $project->getId(),
'success' => $success,
@ -556,7 +555,7 @@ App::get('/v1/vcs/github/authorize')
$appName = System::getEnv('_APP_VCS_GITHUB_APP_NAME');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $platform['consoleDomain'] ?? '';
if (empty($appName)) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'GitHub App name is not configured. Please configure VCS (Version Control System) variables in .env file.');
@ -585,10 +584,10 @@ App::get('/v1/vcs/github/callback')
->inject('gitHub')
->inject('user')
->inject('project')
->inject('request')
->inject('response')
->inject('dbForPlatform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Request $request, Response $response, Database $dbForPlatform) {
->inject('platform')
->action(function (string $providerInstallationId, string $setupAction, string $state, string $code, GitHub $github, Document $user, Document $project, Response $response, Database $dbForPlatform, array $platform) {
if (empty($state)) {
$error = 'Installation requests from organisation members for the Appwrite GitHub App are currently unsupported. To proceed with the installation, login to the Appwrite Console and install the GitHub App.';
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, $error);
@ -615,7 +614,7 @@ App::get('/v1/vcs/github/callback')
$region = $project->getAttribute('region', 'default');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $platform['consoleDomain'] ?? '';
$defaultState = [
'success' => $protocol . '://' . $hostname . "/console/project-$region-$projectId/settings/git-installations",
@ -1479,8 +1478,9 @@ App::post('/v1/vcs/github/events')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->inject('platform')
->action(
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
function (GitHub $github, Request $request, Response $response, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$payload = $request->getRawPayload();
$signatureRemote = $request->getHeader('x-hub-signature-256', '');
$signatureLocal = System::getEnv('_APP_VCS_GITHUB_WEBHOOK_SECRET', '');
@ -1523,7 +1523,7 @@ App::post('/v1/vcs/github/events')
// create new deployment only on push (not committed by us) and not when branch is created or deleted
if ($providerCommitAuthorEmail !== APP_VCS_GITHUB_EMAIL && !$providerBranchCreated && !$providerBranchDeleted) {
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthorName, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, '', false, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
}
} elseif ($event == $github::EVENT_INSTALLATION) {
if ($parsedPayload["action"] == "deleted") {
@ -1579,7 +1579,7 @@ App::post('/v1/vcs/github/events')
Query::orderDesc('$createdAt')
]));
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, $external, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
} elseif ($parsedPayload["action"] == "closed") {
// Allowed external contributions cleanup
@ -1783,13 +1783,13 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
->param('repositoryId', '', new Text(256), 'VCS Repository Id')
->param('providerPullRequestId', '', new Text(256), 'GitHub Pull Request Id')
->inject('gitHub')
->inject('request')
->inject('response')
->inject('project')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('queueForBuilds')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Request $request, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds) use ($createGitDeployments) {
->inject('platform')
->action(function (string $installationId, string $repositoryId, string $providerPullRequestId, GitHub $github, Response $response, Document $project, Database $dbForPlatform, callable $getProjectDB, Build $queueForBuilds, array $platform) use ($createGitDeployments) {
$installation = $dbForPlatform->getDocument('installations', $installationId);
if ($installation->isEmpty()) {
@ -1837,8 +1837,16 @@ App::patch('/v1/vcs/github/installations/:installationId/repositories/:repositor
$providerBranch = \explode(':', $pullRequestResponse['head']['label'])[1] ?? '';
$providerCommitHash = $pullRequestResponse['head']['sha'] ?? '';
$providerBranchUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryName = $pullRequestResponse['head']['repo']['name'] ?? '';
$providerRepositoryUrl = $pullRequestResponse['head']['repo']['html_url'] ?? '';
$providerRepositoryOwner = $pullRequestResponse['head']['repo']['owner']['login'] ?? '';
$providerCommitAuthor = $pullRequestResponse['head']['user']['login'] ?? '';
$providerCommitAuthorUrl = $pullRequestResponse['head']['user']['html_url'] ?? '';
$providerCommitMessage = $pullRequestResponse['title'] ?? '';
$providerCommitUrl = $pullRequestResponse['html_url'] ?? '';
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerCommitHash, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $request);
$createGitDeployments($github, $providerInstallationId, $repositories, $providerBranch, $providerBranchUrl, $providerRepositoryName, $providerRepositoryUrl, $providerRepositoryOwner, $providerCommitHash, $providerCommitAuthor, $providerCommitAuthorUrl, $providerCommitMessage, $providerCommitUrl, $providerPullRequestId, true, $dbForPlatform, $queueForBuilds, $getProjectDB, $platform);
$response->noContent();
});

View file

@ -10,7 +10,7 @@ use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\StatsUsage;
use Appwrite\Extend\Exception as AppwriteException;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Cors;
use Appwrite\Platform\Appwrite;
use Appwrite\SDK\Method;
use Appwrite\SDK\Response as SDKResponse;
@ -39,6 +39,7 @@ use Utopia\Config\Config;
use Utopia\Database\Database;
use Utopia\Database\DateTime;
use Utopia\Database\Document;
use Utopia\Database\Exception\Duplicate;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
@ -51,33 +52,35 @@ use Utopia\Logger\Log\User;
use Utopia\Logger\Logger;
use Utopia\Platform\Service;
use Utopia\System\System;
use Utopia\Validator;
use Utopia\Validator\Text;
Config::setParam('domainVerification', false);
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey)
function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, array $domains)
{
$host = $request->getHostname() ?? '';
if (!empty($previewHostname)) {
$host = $previewHostname;
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = Authorization::skip(fn () => $dbForPlatform->getDocument('rules', md5($host)));
} else {
$rule = Authorization::skip(
fn () => $dbForPlatform->find('rules', [
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? new Document();
}
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = Authorization::skip(function () use ($dbForPlatform, $host, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($host));
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$host]),
]) ?? new Document();
});
$errorView = __DIR__ . '/../views/general/error.phtml';
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . '://' . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$url = $protocol . '://' . $platform['consoleDomain'];
if ($rule->isEmpty()) {
$appDomainFunctionsFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
@ -98,10 +101,8 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
throw $exception;
}
if (System::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL && $host !== System::getEnv('_APP_CONSOLE_DOMAIN', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
if (!in_array($host, $domains)) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'Router protection does not allow accessing Appwrite over this domain. Please add it as custom domain to your project or disable _APP_OPTIONS_ROUTER_PROTECTION environment variable.', view: $errorView);
}
// Act as API - no Proxy logic
@ -143,7 +144,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
if ($type === 'deployment') {
if (System::getEnv('_APP_OPTIONS_ROUTER_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && $request->getHostname() !== APP_HOSTNAME_INTERNAL) {
if ($request->getProtocol() !== 'https') {
if ($request->getMethod() !== Request::METHOD_GET) {
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.', view: $errorView);
}
@ -268,7 +269,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
}
if (!$authorized) {
$url = (System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https') . "://" . System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$url = $protocol . "://" . $platform['consoleDomain'];
$response
->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0')
->addHeader('Pragma', 'no-cache')
@ -456,14 +457,10 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
// Appwrite vars
if ($type === 'function') {
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_FUNCTION_ID' => $resource->getId(),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),
@ -475,7 +472,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
]);
} elseif ($type === 'site') {
$vars = \array_merge($vars, [
'APPWRITE_SITE_API_ENDPOINT' => $endpoint,
'APPWRITE_SITE_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_SITE_ID' => $resource->getId(),
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
'APPWRITE_SITE_DEPLOYMENT' => $deployment->getId(),
@ -845,34 +842,31 @@ App::init()
->inject('request')
->inject('response')
->inject('log')
->inject('console')
->inject('project')
->inject('dbForPlatform')
->inject('getProjectDB')
->inject('locale')
->inject('localeCodes')
->inject('platforms')
->inject('geodb')
->inject('queueForStatsUsage')
->inject('queueForEvents')
->inject('queueForCertificates')
->inject('queueForFunctions')
->inject('executor')
->inject('platform')
->inject('isResourceBlocked')
->inject('previewHostname')
->inject('devKey')
->inject('apiKey')
->inject('httpReferrer')
->inject('httpReferrerSafe')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $console, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, array $platforms, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Certificate $queueForCertificates, Func $queueForFunctions, Executor $executor, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, string $httpReferrer, string $httpReferrerSafe) {
->inject('cors')
->inject('domains')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Document $project, Database $dbForPlatform, callable $getProjectDB, Locale $locale, array $localeCodes, Reader $geodb, StatsUsage $queueForStatsUsage, Event $queueForEvents, Func $queueForFunctions, Executor $executor, array $platform, callable $isResourceBlocked, string $previewHostname, Document $devKey, ?Key $apiKey, Cors $cors, array $domains) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$hostname = $request->getHostname() ?? '';
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (!in_array($hostname, $domains) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey, $domains)) {
$utopia->getRoute()?->label('router', true);
}
}
@ -912,98 +906,12 @@ App::init()
}
}
$domain = $request->getHostname();
$domains = Config::getParam('domains', []);
if (!array_key_exists($domain, $domains)) {
$domain = new Domain(!empty($domain) ? $domain : '');
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$domains[$domain->get()] = false;
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
} elseif (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
} else {
Authorization::disable();
$envDomain = System::getEnv('_APP_DOMAIN', '');
$mainDomain = null;
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($envDomain));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [Query::orderAsc('$id')]);
}
$mainDomain = !$domainDocument->isEmpty() ? $domainDocument->getAttribute('domain') : $domain->get();
}
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$domainDocument = $dbForPlatform->getDocument('rules', md5($domain->get()));
} else {
$domainDocument = $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()])
]);
}
$owner = '';
$functionsDomainFallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($functionsDomainFallback) && \str_ends_with($host, $functionsDomainFallback)) {
$functionsDomain = $functionsDomainFallback;
}
if (
(!empty($functionsDomain) && \str_ends_with($domain->get(), $functionsDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
$owner = 'Appwrite';
}
if ($domainDocument->isEmpty()) {
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
$domainDocument = new Document([
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
$domainDocument = $dbForPlatform->createDocument('rules', $domainDocument);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($domainDocument)
->setSkipRenewCheck(true)
->trigger();
}
}
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
}
Config::setParam('domains', $domains);
}
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
if (\in_array($localeParam, $localeCodes)) {
$locale->setDefault($localeParam);
}
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
$origin = \parse_url($request->getOrigin($request->getReferer('')), PHP_URL_HOST);
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
@ -1032,8 +940,8 @@ App::init()
$warnings = [];
/*
* Response format
*/
* Response format
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', System::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
if (version_compare($responseFormat, '1.4.0', '<')) {
@ -1053,14 +961,13 @@ App::init()
}
}
/*
* Security Headers
*
* As recommended at:
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
// Add Appwrite warning headers
if (!empty($warnings)) {
$response->addHeader('X-Appwrite-Warning', implode(';', $warnings));
}
if (System::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' && ($swooleRequest->header['host'] ?? '') !== 'localhost') { // 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.');
}
@ -1068,49 +975,150 @@ App::init()
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
}
}
});
if ($request->getProtocol() === 'https') {
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
/**
* Security headers
*
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
App::init()
->groups(['api', 'web'])
->inject('request')
->inject('response')
->inject('cors')
->inject('devKey')
->inject('originValidator')
->action(function (Request $request, Response $response, Cors $cors, Document $devKey, Validator $originValidator) {
// CORS headers
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response->addHeader($name, $value);
}
// Security headers
$response
->addHeader('Server', 'Appwrite')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $httpReferrerSafe)
->addHeader('Access-Control-Allow-Credentials', 'true');
->addHeader('X-Content-Type-Options', 'nosniff');
if (!$devKey->isEmpty()) {
$response->addHeader('Access-Control-Allow-Origin', '*');
if ($request->getProtocol() === 'https') {
$maxAge = 60 * 60 * 24 * 126; // 126 days
$response->addHeader('Strict-Transport-Security', "max-age=$maxAge");
}
if (!empty($warnings)) {
$response->addHeader('X-Appwrite-Warning', implode(';', $warnings));
// Application level CSRF protection
$origin = $request->getOrigin();
if (empty($origin) || !$devKey->isEmpty() || !empty($request->getHeader('x-appwrite-key'))) {
return;
}
/*
* Validate Client Domain - Check to avoid CSRF attack
* Adding Appwrite API domains to allow XDOMAIN communication
* Skip this check for non-web platforms which are not required to send an origin header
*/
$origin = $request->getOrigin($request->getReferer(''));
$originValidator = new Origin($platforms);
if (
$devKey->isEmpty()
&& !empty($origin)
&& !$originValidator->isValid($origin)
&& \in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
&& $route->getLabel('origin', false) !== '*'
&& empty($request->getHeader('x-appwrite-key', ''))
&& \parse_url($httpReferrerSafe, PHP_URL_HOST) === 'localhost'
) {
$route = $request->getRoute();
if ($route->getLabel('origin', false) === '*') {
return;
}
if (!$originValidator->isValid($origin)) {
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
}
});
/**
* Automatic certificate generation
*/
App::init()
->groups(['api', 'web'])
->inject('request')
->inject('console')
->inject('dbForPlatform')
->inject('queueForCertificates')
->inject('domains')
->action(function (Request $request, Document $console, Database $dbForPlatform, Certificate $queueForCertificates, array $domains) {
$hostname = $request->getHostname();
$cache = Config::getParam('domains', []);
// 1. Cache hit
if (array_key_exists($hostname, $cache)) {
return;
}
// 2. Domain validation
$domain = new Domain(!empty($hostname) ? $hostname : '');
if (empty($domain->get()) || !$domain->isKnown() || $domain->isTest()) {
$cache[$domain->get()] = false;
Config::setParam('domains', $cache);
Console::warning($domain->get() . ' is not a publicly accessible domain. Skipping SSL certificate generation.');
return;
}
if (str_starts_with($request->getURI(), '/.well-known/acme-challenge')) {
Console::warning('Skipping SSL certificates generation on ACME challenge.');
return;
}
// 3. Check if domain is a main domain
if (!in_array($domain->get(), $domains)) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
return;
}
// 4. Check/create rule (requires DB access)
Authorization::disable();
try {
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$document = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain->get()))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain->get()]),
]);
if (!$document->isEmpty()) {
return;
}
// 5. Create new rule
$owner = '';
$fallback = System::getEnv('_APP_DOMAIN_FUNCTIONS_FALLBACK', '');
$funcDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
$siteDomain = System::getEnv('_APP_DOMAIN_SITES', '');
if (!empty($fallback) && \str_ends_with($domain->get(), $fallback)) {
$funcDomain = $fallback;
}
if (
(!empty($funcDomain) && \str_ends_with($domain->get(), $funcDomain)) ||
(!empty($siteDomain) && \str_ends_with($domain->get(), $siteDomain))
) {
$owner = 'Appwrite';
}
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$document = new Document([
'$id' => $ruleId,
'domain' => $domain->get(),
'type' => 'api',
'status' => 'verifying',
'projectId' => $console->getId(),
'projectInternalId' => $console->getSequence(),
'search' => implode(' ', [$ruleId, $domain->get()]),
'owner' => $owner,
'region' => $console->getAttribute('region')
]);
$dbForPlatform->createDocument('rules', $document);
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
$queueForCertificates
->setDomain($document)
->setSkipRenewCheck(true)
->trigger();
} catch (Duplicate $e) {
Console::info('Certificate already exists');
} finally {
$cache[$domain->get()] = true;
Config::setParam('domains', $cache);
Authorization::reset();
}
});
App::options()
->inject('utopia')
->inject('swooleRequest')
@ -1125,38 +1133,32 @@ App::options()
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('platform')
->inject('previewHostname')
->inject('project')
->inject('devKey')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey) {
->inject('domains')
->inject('cors')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, Document $project, Document $devKey, ?Key $apiKey, array $domains, Cors $cors) {
/*
* Appwrite Router
*/
$host = $request->getHostname() ?? '';
$mainDomain = System::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (!in_array($request->getHostname(), $domains) || !empty($previewHostname)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
$origin = $request->getOrigin();
foreach ($cors->headers($request->getOrigin()) as $name => $value) {
$response->addHeader($name, $value);
}
$response
->addHeader('Server', 'Appwrite')
->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE')
->addHeader('Access-Control-Allow-Headers', 'Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent')
->addHeader('Access-Control-Expose-Headers', 'X-Appwrite-Session, X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();
if (!$devKey->isEmpty()) {
$response->addHeader('Access-Control-Allow-Origin', '*');
}
/** OPTIONS requests in utopia do not execute shutdown handlers, as a result we need to track the OPTIONS requests explicitly
* @see https://github.com/utopia-php/http/blob/0.33.16/src/App.php#L825-L855
*/
@ -1443,18 +1445,16 @@ App::get('/robots.txt')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
->inject('domains')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, array $domains) {
if (in_array($request->getHostname(), $domains) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/robots.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}
@ -1477,18 +1477,16 @@ App::get('/humans.txt')
->inject('executor')
->inject('geodb')
->inject('isResourceBlocked')
->inject('platform')
->inject('previewHostname')
->inject('apiKey')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, string $previewHostname, ?Key $apiKey) {
$host = $request->getHostname() ?? '';
$consoleDomain = System::getEnv('_APP_CONSOLE_DOMAIN', '');
$mainDomain = System::getEnv('_APP_DOMAIN', '');
if (($host === $consoleDomain || $host === $mainDomain || $host === 'localhost') && empty($previewHostname)) {
->inject('domains')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Log $log, Database $dbForPlatform, callable $getProjectDB, Event $queueForEvents, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Executor $executor, Reader $geodb, callable $isResourceBlocked, array $platform, string $previewHostname, ?Key $apiKey, array $domains) {
if (in_array($request->getHostname(), $domains) || !empty($previewHostname)) {
$template = new View(__DIR__ . '/../views/general/humans.phtml');
$response->text($template->render(false));
} else {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $previewHostname, $apiKey)) {
if (router($utopia, $dbForPlatform, $getProjectDB, $swooleRequest, $request, $response, $log, $queueForEvents, $queueForStatsUsage, $queueForFunctions, $executor, $geodb, $isResourceBlocked, $platform, $previewHostname, $apiKey)) {
$utopia->getRoute()?->label('router', true);
}
}

View file

@ -15,7 +15,6 @@ use Utopia\Database\Helpers\Role;
use Utopia\Database\Validator\UID;
use Utopia\Locale\Locale;
use Utopia\System\System;
use Utopia\Validator\Host;
use Utopia\Validator\Text;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub;
@ -27,7 +26,7 @@ App::get('/v1/mock/tests/general/oauth2')
->label('docs', false)
->label('mock', true)
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.') // Important to deny an open redirect attack
->param('redirect_uri', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator']) // Important to deny an open redirect attack
->param('scope', '', new Text(100), 'OAuth2 scope list.')
->param('state', '', new Text(1024), 'OAuth2 state.')
->inject('response')
@ -64,7 +63,7 @@ App::get('/v1/mock/tests/general/oauth2/token')
->param('client_id', '', new Text(100), 'OAuth2 Client ID.')
->param('client_secret', '', new Text(100), 'OAuth2 scope list.')
->param('grant_type', 'authorization_code', new WhiteList(['refresh_token', 'authorization_code']), 'OAuth2 Grant Type.', true)
->param('redirect_uri', '', new Host(['localhost']), 'OAuth2 Redirect URI.', true)
->param('redirect_uri', '', fn ($redirectValidator) => $redirectValidator, 'URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project\'s platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API.', true, ['redirectValidator'])
->param('code', '', new Text(100), 'OAuth2 state.', true)
->param('refresh_token', '', new Text(100), 'OAuth2 refresh token.', true)
->inject('response')

View file

@ -8,7 +8,9 @@ use Appwrite\Event\Database as EventDatabase;
use Appwrite\Event\Delete;
use Appwrite\Event\Event;
use Appwrite\Event\Func;
use Appwrite\Event\Mail;
use Appwrite\Event\Messaging;
use Appwrite\Event\Migration;
use Appwrite\Event\Realtime;
use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
@ -495,6 +497,9 @@ App::init()
->inject('queueForDatabase')
->inject('queueForBuilds')
->inject('queueForStatsUsage')
->inject('queueForFunctions')
->inject('queueForMails')
->inject('queueForMigrations')
->inject('dbForProject')
->inject('timelimit')
->inject('resourceToken')
@ -503,7 +508,8 @@ App::init()
->inject('plan')
->inject('devKey')
->inject('telemetry')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry) use ($usageDatabaseListener, $eventDatabaseListener) {
->inject('platform')
->action(function (App $utopia, Request $request, Response $response, Document $project, Document $user, Publisher $publisher, Publisher $publisherFunctions, Publisher $publisherWebhooks, Event $queueForEvents, Messaging $queueForMessaging, Audit $queueForAudits, Delete $queueForDeletes, EventDatabase $queueForDatabase, Build $queueForBuilds, StatsUsage $queueForStatsUsage, Func $queueForFunctions, Mail $queueForMails, Migration $queueForMigrations, Database $dbForProject, callable $timelimit, Document $resourceToken, string $mode, ?Key $apiKey, array $plan, Document $devKey, Telemetry $telemetry, array $platform) use ($usageDatabaseListener, $eventDatabaseListener) {
$route = $utopia->getRoute();
@ -577,6 +583,10 @@ App::init()
}
}
/**
* TODO: (@loks0n)
* Avoid mutating the message across file boundaries - it's difficult to reason about at scale.
*/
/*
* Background Jobs
*/
@ -607,10 +617,18 @@ App::init()
}
}
/* Auto-set projects */
$queueForDeletes->setProject($project);
$queueForDatabase->setProject($project);
$queueForBuilds->setProject($project);
$queueForMessaging->setProject($project);
$queueForFunctions->setProject($project);
$queueForBuilds->setProject($project);
/* Auto-set platforms */
$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.

View file

@ -10,7 +10,7 @@ App::get('/versions')
->label('scope', 'public')
->inject('response')
->action(function (Response $response) {
$platforms = Config::getParam('platforms');
$platforms = Config::getParam('sdks');
$versions = [
'server' => APP_VERSION_STABLE,

View file

@ -15,7 +15,8 @@ Config::load('auth', __DIR__ . '/../config/auth.php', $configAdapter);
Config::load('apis', __DIR__ . '/../config/apis.php', $configAdapter); // List of APIs
Config::load('errors', __DIR__ . '/../config/errors.php', $configAdapter);
Config::load('oAuthProviders', __DIR__ . '/../config/oAuthProviders.php', $configAdapter);
Config::load('platforms', __DIR__ . '/../config/platforms.php', $configAdapter);
Config::load('sdks', __DIR__ . '/../config/sdks.php', $configAdapter);
Config::load('platform', __DIR__ . '/../config/platform.php', $configAdapter);
Config::load('console', __DIR__ . '/../config/console.php', $configAdapter);
Config::load('collections', __DIR__ . '/../config/collections.php', $configAdapter);
Config::load('frameworks', __DIR__ . '/../config/frameworks.php', $configAdapter);

View file

@ -4,12 +4,17 @@ use Appwrite\Platform\Modules\Compute\Specification;
const APP_NAME = 'Appwrite';
const APP_DOMAIN = 'appwrite.io';
// Email
const APP_EMAIL_TEAM = 'team@localhost.test'; // Default email address
const APP_EMAIL_SECURITY = ''; // Default security email address
const APP_EMAIL_LOGO_URL = 'https://cloud.appwrite.io/images/mails/logo.png';
const APP_EMAIL_ACCENT_COLOR = '#fd366e';
const APP_EMAIL_TERMS_URL = 'https://appwrite.io/terms';
const APP_EMAIL_PRIVACY_URL = 'https://appwrite.io/privacy';
const APP_EMAIL_PLATFORM_NAME = 'Appwrite';
const APP_EMAIL_FOOTER_IMAGE_URL = 'https://appwrite.io/email/footer.png';
const APP_USERAGENT = APP_NAME . '-Server v%s. Please report abuse at %s';
const APP_MODE_DEFAULT = 'default';
const APP_MODE_ADMIN = 'admin';
@ -81,7 +86,6 @@ const APP_SOCIAL_DISCORD_CHANNEL = '564160730845151244';
const APP_SOCIAL_DEV = 'https://dev.to/appwrite';
const APP_SOCIAL_STACKSHARE = 'https://stackshare.io/appwrite';
const APP_SOCIAL_YOUTUBE = 'https://www.youtube.com/c/appwrite?sub_confirmation=1';
const APP_HOSTNAME_INTERNAL = 'appwrite';
const APP_COMPUTE_CPUS_DEFAULT = 0.5;
const APP_COMPUTE_MEMORY_DEFAULT = 512;
const APP_COMPUTE_SPECIFICATION_DEFAULT = Specification::S_1VCPU_512MB;

View file

@ -20,8 +20,10 @@ use Appwrite\Event\StatsUsage;
use Appwrite\Event\Webhook;
use Appwrite\Extend\Exception;
use Appwrite\GraphQL\Schema;
use Appwrite\Network\Cors;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use Appwrite\Network\Validator\Redirect;
use Appwrite\Utopia\Database\Documents\User;
use Appwrite\Utopia\Request;
use Appwrite\Utopia\Response;
@ -43,7 +45,6 @@ use Utopia\Database\Adapter\Pool as DatabasePool;
use Utopia\Database\Database;
use Utopia\Database\DateTime as DatabaseDateTime;
use Utopia\Database\Document;
use Utopia\Database\Helpers\ID;
use Utopia\Database\Query;
use Utopia\Database\Validator\Authorization;
use Utopia\DSN\DSN;
@ -64,7 +65,7 @@ use Utopia\Storage\Storage;
use Utopia\System\System;
use Utopia\Telemetry\Adapter as Telemetry;
use Utopia\Telemetry\Adapter\None as NoTelemetry;
use Utopia\Validator\Hostname;
use Utopia\Validator\URL;
use Utopia\Validator\WhiteList;
use Utopia\VCS\Adapter\Git\GitHub as VcsGitHub;
@ -159,79 +160,189 @@ App::setResource('queueForMigrations', function (Publisher $publisher) {
App::setResource('queueForStatsResources', function (Publisher $publisher) {
return new StatsResources($publisher);
}, ['publisher']);
App::setResource('platforms', function (Request $request, Document $console, Document $project, Database $dbForPlatform) {
$console->setAttribute('platforms', [ // Always allow current host
'$collection' => ID::custom('platforms'),
'name' => 'Current Host',
'type' => Platform::TYPE_WEB,
'hostname' => $request->getHostname(),
], Document::SET_TYPE_APPEND);
$hostnames = explode(',', System::getEnv('_APP_CONSOLE_HOSTNAMES', ''));
$validator = new Hostname();
foreach ($hostnames as $hostname) {
$hostname = trim($hostname);
if (!$validator->isValid($hostname)) {
continue;
}
$console->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_WEB,
'name' => $hostname,
'hostname' => $hostname,
], Document::SET_TYPE_APPEND);
/**
* List of domains served by the application.
*/
App::setResource('domains', fn () => array_unique(array_filter([
...\explode(',', System::getEnv('_APP_DOMAIN', 'localhost')),
...\explode(',', System::getEnv('_APP_CONSOLE_DOMAIN', 'localhost'))
])));
/**
* Platform configuration
*/
App::setResource('platform', function (Request $request) {
$platform = Config::getParam('platform', []);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = '';
if ($request->getPort() === '443' && $protocol !== 'https') {
$port = ':443';
}
if ($request->getPort() === '80' && $protocol !== 'http') {
$port = ':80';
}
$platform['endpoint'] = "$protocol://{$platform['domain']}{$port}/v1";
return $platform;
}, ['request']);
/**
* Safe request origin used to construct urls
*/
App::setResource('origin', function (Request $request) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$port = '';
if ($request->getPort() === '443' && $protocol !== 'https') {
$port = ':443';
}
if ($request->getPort() === '80' && $protocol !== 'http') {
$port = ':80';
}
// Add `exp` and `appwrite-callback-{projectId}` schemes
return "$protocol://{$request->getHostname()}{$port}";
}, ['request']);
/**
* List of allowed request hostnames for the request.
*/
App::setResource('allowedHostnames', function (array $domains, Document $project, Document $rule, Document $devKey, Request $request) {
$allowed = [...$domains];
/* Add platform configured hostnames */
if (!$project->isEmpty() && $project->getId() !== 'console') {
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_SCHEME,
'name' => 'Expo',
'key' => 'exp',
], Document::SET_TYPE_APPEND);
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_SCHEME,
'name' => 'Appwrite Callback',
'key' => 'appwrite-callback-' . $project->getId(),
], Document::SET_TYPE_APPEND);
$platforms = $project->getAttribute('platforms', []);
$hostnames = Platform::getHostnames($platforms);
$allowed = [...$allowed, ...$hostnames];
}
$origin = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($origin)) {
$origin = \parse_url($request->getReferer(), PHP_URL_HOST);
/* Add the request hostname if a dev key is found */
if (!$devKey->isEmpty()) {
$allowed[] = $request->getHostname();
}
// Safe if rule with same project ID exists
if (!empty($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();
/* Allow the request origin if a dev key or rule is found */
$originHostname = parse_url($request->getOrigin(), PHP_URL_HOST);
if ((!$rule->isEmpty() || !$devKey->isEmpty()) && !empty($originHostname)) {
$allowed[] = $originHostname;
}
return array_unique($allowed);
}, ['domains', 'project', 'rule', 'devKey', 'request']);
/**
* List of allowed request schemes for the request.
*/
App::setResource('allowedSchemes', function (Document $project) {
$allowed = [];
if (!$project->isEmpty() && $project->getId() !== 'console') {
/* Add hardcoded schemes */
$allowed[] = 'exp';
$allowed[] = 'appwrite-callback-' . $project->getId();
/* Add platform configured schemes */
$platforms = $project->getAttribute('platforms', []);
$schemes = Platform::getSchemes($platforms);
$allowed = [...$allowed, ...$schemes];
}
return array_unique($allowed);
}, ['project']);
/**
* Rule associated with a request origin.
*/
App::setResource('rule', function (Request $request, Database $dbForPlatform, Document $project) {
$domain = \parse_url($request->getOrigin(), PHP_URL_HOST);
if (empty($domain)) {
return new Document();
}
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = Authorization::skip(function () use ($dbForPlatform, $domain, $isMd5) {
if ($isMd5) {
return $dbForPlatform->getDocument('rules', md5($domain));
}
if (!$rule->isEmpty() && $rule->getAttribute('projectInternalId') === $project->getSequence()) {
$project->setAttribute('platforms', [
'$collection' => ID::custom('platforms'),
'type' => Platform::TYPE_WEB,
'name' => $origin,
'hostname' => $origin,
], Document::SET_TYPE_APPEND);
}
return $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]) ?? new Document();
});
if ($rule->getAttribute('projectInternalId') !== $project->getSequence()) {
return new Document();
}
return [
...$console->getAttribute('platforms', []),
...$project->getAttribute('platforms', []),
];
}, ['request', 'console', 'project', 'dbForPlatform']);
return $rule;
}, ['request', 'dbForPlatform', 'project']);
/**
* CORS service
*/
App::setResource('cors', fn (array $allowedHostnames) => new Cors(
$allowedHostnames,
allowedMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: [
'Accept',
'Origin',
'Cookie',
'Set-Cookie',
// Content
'Content-Type',
'Content-Range',
// Appwrite
'X-Appwrite-Project',
'X-Appwrite-Key',
'X-Appwrite-Dev-Key',
'X-Appwrite-Locale',
'X-Appwrite-Mode',
'X-Appwrite-JWT',
'X-Appwrite-Response-Format',
'X-Appwrite-Timeout',
'X-Appwrite-ID',
'X-Appwrite-Timestamp',
'X-Appwrite-Session',
// SDK generator
'X-SDK-Version',
'X-SDK-Name',
'X-SDK-Language',
'X-SDK-Platform',
'X-SDK-GraphQL',
// Caching
'Range',
'Cache-Control',
'Expires',
'Pragma',
// Server to server
'X-Fallback-Cookies',
'X-Requested-With',
'X-Forwarded-For',
'X-Forwarded-User-Agent',
],
allowCredentials: true,
exposedHeaders: [
'X-Appwrite-Session',
'X-Fallback-Cookies',
],
), ['allowedHostnames']);
App::setResource('originValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (!$devKey->isEmpty()) {
return new URL();
}
return new Origin($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
App::setResource('redirectValidator', function (Document $devKey, array $allowedHostnames, array $allowedSchemes) {
if (!$devKey->isEmpty()) {
return new URL();
}
return new Redirect($allowedHostnames, $allowedSchemes);
}, ['devKey', 'allowedHostnames', 'allowedSchemes']);
App::setResource('user', function (string $mode, Document $project, Document $console, Request $request, Response $response, Database $dbForProject, Database $dbForPlatform, Store $store, Token $proofForToken) {
/**
@ -738,7 +849,7 @@ App::setResource('passwordsDictionary', function ($register) {
App::setResource('servers', function () {
$platforms = Config::getParam('platforms');
$platforms = Config::getParam('sdks');
$server = $platforms[APP_PLATFORM_SERVER];
$languages = array_map(function ($language) {
@ -839,24 +950,6 @@ App::setResource('schema', function ($utopia, $dbForProject) {
);
}, ['utopia', 'dbForProject']);
App::setResource('contributors', function () {
$path = 'app/config/contributors.json';
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
return $list;
});
App::setResource('employees', function () {
$path = 'app/config/employees.json';
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
return $list;
});
App::setResource('heroes', function () {
$path = 'app/config/heroes.json';
$list = (file_exists($path)) ? json_decode(file_get_contents($path), true) : [];
return $list;
});
App::setResource('gitHub', function (Cache $cache) {
return new VcsGitHub($cache);
}, ['cache']);
@ -923,6 +1016,7 @@ App::setResource('devKey', function (Request $request, Document $project, array
$dbForPlatform->purgeCachedDocument('projects', $project->getId());
}
}
return $key;
}, ['request', 'project', 'servers', 'dbForPlatform']);
@ -1063,37 +1157,6 @@ App::setResource('resourceToken', function ($project, $dbForProject, $request) {
return new Document([]);
}, ['project', 'dbForProject', 'request']);
App::setResource('httpReferrer', function (Request $request): string {
$referrer = $request->getReferer();
return $referrer;
}, ['request']);
App::setResource('httpReferrerSafe', function (Request $request, string $httpReferrer, array $platforms, Database $dbForPlatform, Document $project, App $utopia): string {
$origin = \parse_url($request->getOrigin($httpReferrer), PHP_URL_HOST);
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
// Safe if route is publicly accessible
$route = $utopia->getRoute();
if ($route->getLabel('origin', false)) {
return $referrer;
}
// Safe if added as web platform
$originValidator = new Origin($platforms);
if ($originValidator->isValid($request->getOrigin($httpReferrer))) {
return $referrer;
}
// Unsafe; Localhost is always safe for ease of local development
$origin = 'localhost';
$protocol = \parse_url($request->getOrigin($httpReferrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($httpReferrer), PHP_URL_PORT);
$referrer = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
return $referrer;
}, ['request', 'httpReferrer', 'platforms', 'dbForPlatform', 'project', 'utopia']);
App::setResource('transactionState', function (Database $dbForProject) {
return new TransactionState($dbForProject);
}, ['dbForProject']);

View file

@ -543,7 +543,6 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
}
$timelimit = $app->getResource('timelimit');
$platforms = $app->getResource('platforms');
$user = $app->getResource('user'); /** @var User $user */
/*
@ -568,7 +567,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
* Skip this check for non-web platforms which are not required to send an origin header.
*/
$origin = $request->getOrigin();
$originValidator = new Origin($platforms);
$originValidator = $app->getResource('originValidator');
if (!empty($origin) && !$originValidator->isValid($origin) && $project->getId() !== 'console') {
throw new Exception(Exception::REALTIME_POLICY_VIOLATION, $originValidator->getDescription());

View file

@ -1,4 +1,5 @@
<?php
use Utopia\Config\Config;
use Utopia\System\System;
$development = $this->getParam('development', false);
@ -15,7 +16,8 @@ $labelClass = '';
$buttons = [];
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$platform = Config::getParam('platform', []);
$hostname = $platform['consoleDomain'] ?? '';
// TODO: remove this later
if (System::getEnv('_APP_ENV') === 'development') {
$hostname = 'localhost';
@ -537,4 +539,4 @@ switch ($type) {
</script>
</body>
</html>
</html>

View file

@ -38,9 +38,15 @@ services:
depends_on:
- appwrite
networks:
- gateway
- appwrite
- runtimes
appwrite:
aliases:
- appwrite.test
gateway:
aliases:
- appwrite.test
runtimes:
aliases:
- appwrite.test
appwrite:
container_name: appwrite

View file

@ -115,7 +115,8 @@ class Build extends Event
'resource' => $this->resource,
'deployment' => $this->deployment,
'type' => $this->type,
'template' => $this->template
'template' => $this->template,
'platform' => $this->platform
];
}
@ -130,6 +131,7 @@ class Build extends Event
$this->resource = null;
$this->deployment = null;
$this->template = null;
$this->platform = [];
parent::reset();
return $this;

View file

@ -52,9 +52,11 @@ class Event
protected array $sensitive = [];
protected array $payload = [];
protected array $context = [];
protected array $platform = [];
protected ?Document $project = null;
protected ?Document $user = null;
protected ?string $userId = null;
protected bool $paused = false;
/** @var bool Non-critical events will not throw an exception when enqueuing of the event fails. */
@ -153,6 +155,28 @@ class Event
return $this->project;
}
/**
* Set platform for this event.
*
* @param array $platform
* @return self
*/
public function setPlatform(array $platform): self
{
$this->platform = $platform;
return $this;
}
/**
* Get platform for this event.
*
* @return array
*/
public function getPlatform(): array
{
return $this->platform;
}
/**
* Set user for this event.
*

View file

@ -161,7 +161,7 @@ class Func extends Event
/**
* Sets custom headers for the function event.
*
* @param string $headers
* @param array $headers
* @return self
*/
public function setHeaders(array $headers): self
@ -217,6 +217,7 @@ class Func extends Event
'path' => $this->path,
'headers' => $this->headers,
'method' => $this->method,
'platform' => $this->platform
];
}
}

View file

@ -77,6 +77,7 @@ class Migration extends Event
'project' => $this->project,
'user' => $this->user,
'migration' => $this->migration,
'platform' => $this->platform,
];
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace Appwrite\Network;
/**
* Generate CORS response headers for an incoming request.
*
* Allowed origins are matched by hostname only. Arrays passed to the
* constructor (methods, headers, exposed headers) are formatted into
* comma-separated header strings.
*/
final class Cors
{
public const string HEADER_ALLOW_ORIGIN = 'Access-Control-Allow-Origin';
public const string HEADER_ALLOW_METHODS = 'Access-Control-Allow-Methods';
public const string HEADER_ALLOW_HEADERS = 'Access-Control-Allow-Headers';
public const string HEADER_ALLOW_CREDENTIALS = 'Access-Control-Allow-Credentials';
public const string HEADER_EXPOSE_HEADERS = 'Access-Control-Expose-Headers';
public const string HEADER_MAX_AGE = 'Access-Control-Max-Age';
/**
* @param array<string> $allowedHosts Array of allowed hosts
* @param array<string> $allowedMethods Array of allowed methods
* @param array<string> $allowedHeaders Array of allowed header
* @param array<string> $exposedHeaders Array of exposed headers
* @param bool $allowCredentials Whether to allow credentials (default: false)
* @param int $maxAge Maximum age of the preflight response (default: 86400 seconds)
*/
public function __construct(
private array $allowedHosts,
private array $allowedMethods,
private array $allowedHeaders,
private array $exposedHeaders,
private bool $allowCredentials = false,
private int $maxAge = 86400,
) {
$this->allowedHosts = \array_map('strtolower', $this->allowedHosts);
if ($this->allowedHosts === ['*'] && $allowCredentials === true) {
throw new \InvalidArgumentException(
'CORS invariant violated: cannot use wildcard origin "*" when credentials are enabled.'
);
}
}
/**
* Build CORS headers for a given request origin.
*
* @return array<string,string>
*/
public function headers(string $origin): array
{
$headers = [
self::HEADER_ALLOW_METHODS => implode(', ', $this->allowedMethods),
self::HEADER_ALLOW_HEADERS => implode(', ', $this->allowedHeaders),
self::HEADER_EXPOSE_HEADERS => implode(', ', $this->exposedHeaders),
self::HEADER_ALLOW_CREDENTIALS => $this->allowCredentials ? 'true' : 'false',
self::HEADER_MAX_AGE => $this->maxAge,
];
// Wildcard allow-all
if ($this->allowedHosts === ['*']) {
$headers[self::HEADER_ALLOW_ORIGIN] = $origin;
return $headers;
}
// Normal origin handling
$origin = strtolower(trim($origin));
if ($origin === '') {
return $headers;
}
$host = parse_url($origin, PHP_URL_HOST);
if (!\is_string($host) || $host === '') {
return $headers;
}
// Match only by host
if (!\in_array($host, $this->allowedHosts, true)) {
return $headers;
}
// Accepted
$headers[self::HEADER_ALLOW_ORIGIN] = $origin;
return $headers;
}
}

View file

@ -8,8 +8,6 @@ use Utopia\Validator\Hostname;
class Origin extends Validator
{
protected array $hostnames = [];
protected array $schemes = [];
protected ?string $scheme = null;
protected ?string $host = null;
protected string $origin = '';
@ -17,12 +15,11 @@ class Origin extends Validator
/**
* Constructor
*
* @param array<\Utopia\Database\Document> $platforms
* @param array<string> $allowedHostnames
* @param array<string> $allowedSchemes
*/
public function __construct(array $platforms)
public function __construct(protected array $allowedHostnames, protected array $allowedSchemes)
{
$this->hostnames = Platform::getHostnames($platforms);
$this->schemes = Platform::getSchemes($platforms);
}
@ -53,11 +50,11 @@ class Origin extends Validator
Platform::SCHEME_EDGE_EXTENSION,
];
if (in_array($this->scheme, $webPlatforms, true)) {
$validator = new Hostname($this->hostnames);
$validator = new Hostname($this->allowedHostnames);
return $validator->isValid($this->host);
}
if (!empty($this->scheme) && in_array($this->scheme, $this->schemes, true)) {
if (!empty($this->scheme) && in_array($this->scheme, $this->allowedSchemes, true)) {
return true;
}

View file

@ -235,8 +235,9 @@ class Base extends Action
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([

View file

@ -59,6 +59,7 @@ class Get extends Action
->param('type', '', new WhiteList(['rules']), 'Resource type.')
->inject('response')
->inject('dbForPlatform')
->inject('domains')
->callback($this->action(...));
}
@ -66,7 +67,8 @@ class Get extends Action
string $value,
string $type,
Response $response,
Database $dbForPlatform
Database $dbForPlatform,
array $domains
) {
if ($type === 'rules') {
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
@ -89,13 +91,7 @@ class Get extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$deniedDomains = [...$domains];
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;

View file

@ -98,6 +98,7 @@ class Create extends Base
->inject('store')
->inject('proofForToken')
->inject('executor')
->inject('platform')
->callback($this->action(...));
}
@ -121,7 +122,8 @@ class Create extends Base
Reader $geodb,
Store $store,
Token $proofForToken,
Executor $executor
Executor $executor,
array $platform
) {
$async = \strval($async) === 'true' || \strval($async) === '1';
@ -366,13 +368,9 @@ class Create extends Base
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_FUNCTION_ID' => $functionId,
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deployment->getId(),

View file

@ -362,8 +362,9 @@ class Create extends Base
if (!empty($functionsDomain)) {
$routeSubdomain = ID::unique();
$domain = "{$routeSubdomain}.{$functionsDomain}";
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
$rule = Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([

View file

@ -129,6 +129,8 @@ class Builds extends Action
$resource = new Document($payload['resource'] ?? []);
$deployment = new Document($payload['deployment'] ?? []);
$template = new Document($payload['template'] ?? []);
$platform = $payload['platform'] ?? [];
$log->addTag('projectId', $project->getId());
$log->addTag('type', $type);
@ -157,7 +159,8 @@ class Builds extends Action
$isResourceBlocked,
$log,
$executor,
$plan
$plan,
$platform
);
break;
@ -209,7 +212,8 @@ class Builds extends Action
callable $isResourceBlocked,
Log $log,
Executor $executor,
array $plan
array $plan,
array $platform
): void {
Console::info('Deployment action started');
@ -532,7 +536,7 @@ class Builds extends Action
Console::log('Git source uploaded');
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
$this->runGitAction('processing', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
Console::log('Status marked as building');
@ -550,7 +554,7 @@ class Builds extends Action
->trigger();
if ($isVcsEnabled) {
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
$this->runGitAction('building', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
$deploymentModel = new Deployment();
@ -612,10 +616,6 @@ class Builds extends Action
'scopes' => $resource->getAttribute('scopes', [])
]);
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_VERSION' => APP_VERSION_STABLE,
@ -639,7 +639,7 @@ class Builds extends Action
case 'functions':
$vars = [
...$vars,
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_FUNCTION_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey,
'APPWRITE_FUNCTION_ID' => $resource->getId(),
'APPWRITE_FUNCTION_NAME' => $resource->getAttribute('name'),
@ -654,7 +654,7 @@ class Builds extends Action
case 'sites':
$vars = [
...$vars,
'APPWRITE_SITE_API_ENDPOINT' => $endpoint,
'APPWRITE_SITE_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_SITE_API_KEY' => API_KEY_DYNAMIC . '_' . $apiKey,
'APPWRITE_SITE_ID' => $resource->getId(),
'APPWRITE_SITE_NAME' => $resource->getAttribute('name'),
@ -1108,7 +1108,7 @@ class Builds extends Action
}
if ($isVcsEnabled) {
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
$this->runGitAction('ready', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
/** Set auto deploy */
@ -1337,7 +1337,7 @@ class Builds extends Action
->trigger();
if ($isVcsEnabled) {
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime);
$this->runGitAction('failed', $github, $providerCommitHash, $owner, $repositoryName, $project, $resource, $deployment->getId(), $dbForProject, $dbForPlatform, $queueForRealtime, $platform);
}
} finally {
$queueForRealtime
@ -1489,6 +1489,8 @@ class Builds extends Action
* @param string $deploymentId
* @param Database $dbForProject
* @param Database $dbForPlatform
* @param Realtime $queueForRealtime
* @param array $platform
* @return void
* @throws Structure
* @throws \Utopia\Database\Exception
@ -1508,6 +1510,7 @@ class Builds extends Action
Database $dbForProject,
Database $dbForPlatform,
Realtime $queueForRealtime,
array $platform
): void {
try {
if ($resource->getAttribute('providerSilentMode', false) === true) {
@ -1593,7 +1596,7 @@ class Builds extends Action
default => throw new \Exception('Invalid resource type')
};
$comment = new Comment();
$comment = new Comment($platform);
$comment->parseComment($github->getComment($owner, $repositoryName, $commentId));
$comment->addBuild($project, $resource, $resourceType, $status, $deployment->getId(), ['type' => 'logs'], $previewUrl);
$github->updateComment($owner, $repositoryName, $commentId, $comment->generateComment());

View file

@ -67,10 +67,11 @@ class Create extends Action
->inject('queueForCertificates')
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('domains')
->callback($this->action(...));
}
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform)
public function action(string $domain, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, array $domains)
{
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@ -90,13 +91,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$deniedDomains = [...$domains];
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
@ -125,8 +120,9 @@ class Create extends Action
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();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {

View file

@ -72,10 +72,11 @@ class Create extends Action
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('domains')
->callback($this->action(...));
}
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
public function action(string $domain, string $functionId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $domains)
{
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@ -95,13 +96,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$deniedDomains = [...$domains];
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
@ -137,8 +132,9 @@ class Create extends Action
$deployment = $dbForProject->getDocument('deployments', $function->getAttribute('deploymentId', ''));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {

View file

@ -75,10 +75,11 @@ class Create extends Action
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('domains')
->callback($this->action(...));
}
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
public function action(string $domain, string $url, int $statusCode, string $resourceId, string $resourceType, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $domains)
{
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@ -98,13 +99,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$deniedDomains = [...$domains];
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
@ -142,8 +137,9 @@ class Create extends Action
throw new Exception(Exception::RULE_RESOURCE_NOT_FOUND);
}
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {

View file

@ -72,10 +72,11 @@ class Create extends Action
->inject('queueForEvents')
->inject('dbForPlatform')
->inject('dbForProject')
->inject('domains')
->callback($this->action(...));
}
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject)
public function action(string $domain, string $siteId, string $branch, Response $response, Document $project, Certificate $queueForCertificates, Event $queueForEvents, Database $dbForPlatform, Database $dbForProject, array $domains)
{
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$functionsDomain = System::getEnv('_APP_DOMAIN_FUNCTIONS', '');
@ -95,13 +96,7 @@ class Create extends Action
throw new Exception(Exception::GENERAL_ARGUMENT_INVALID, 'This domain name is not allowed. Please use a different domain.');
}
$deniedDomains = [
'localhost',
APP_HOSTNAME_INTERNAL
];
$mainDomain = System::getEnv('_APP_DOMAIN', '');
$deniedDomains[] = $mainDomain;
$deniedDomains = [...$domains];
if (!empty($sitesDomain)) {
$deniedDomains[] = $sitesDomain;
@ -137,8 +132,9 @@ class Create extends Action
$deployment = $dbForProject->getDocument('deployments', $site->getAttribute('deploymentId', ''));
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain->get()) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain->get()) : ID::unique();
$status = 'created';
if (\str_ends_with($domain->get(), $functionsDomain) || \str_ends_with($domain->get(), $sitesDomain)) {

View file

@ -272,8 +272,9 @@ class Create extends Action
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([

View file

@ -143,8 +143,9 @@ class Create extends Action
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([

View file

@ -53,7 +53,7 @@ class Create extends Base
name: 'createTemplateDeployment',
description: <<<EOT
Create a deployment based on a template.
Use this endpoint with combination of [listTemplates](https://appwrite.io/docs/products/sites/templates) to find the template details.
EOT,
auth: [AuthType::KEY],
@ -185,8 +185,9 @@ class Create extends Base
$sitesDomain = System::getEnv('_APP_DOMAIN_SITES', '');
$domain = ID::unique() . "." . $sitesDomain;
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
$ruleId = System::getEnv('_APP_RULES_FORMAT') === 'md5' ? md5($domain) : ID::unique();
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$ruleId = $isMd5 ? md5($domain) : ID::unique();
Authorization::skip(
fn () => $dbForPlatform->createDocument('rules', new Document([

View file

@ -129,15 +129,16 @@ class Maintenance extends Action
if (\count($certificates) > 0) {
Console::info("[{$time}] Found " . \count($certificates) . " certificates for renewal, scheduling jobs.");
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
foreach ($certificates as $certificate) {
$domain = $certificate->getAttribute('domain');
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$rule = $dbForPlatform->findOne('rules', [
$rule = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if ($rule->isEmpty() || $rule->getAttribute('region') !== System::getEnv('_APP_REGION', 'default')) {
continue;

View file

@ -107,7 +107,7 @@ class SDKs extends Action
throw new \Exception('Unknown version given');
}
$platforms = Config::getParam('platforms');
$platforms = Config::getParam('sdks');
foreach ($platforms as $key => $platform) {
if ($selectedPlatform !== $key && $selectedPlatform !== '*' && ($sdks === null)) {
continue;

View file

@ -430,14 +430,13 @@ class Certificates extends Action
Func $queueForFunctions,
Realtime $queueForRealtime
): void {
// TODO: @christyjacob remove once we migrate the rules in 1.7.x
if (System::getEnv('_APP_RULES_FORMAT') === 'md5') {
$rule = $dbForPlatform->getDocument('rules', md5($domain));
} else {
$rule = $dbForPlatform->findOne('rules', [
// TODO: (@Meldiron) Remove after 1.7.x migration
$isMd5 = System::getEnv('_APP_RULES_FORMAT') === 'md5';
$rule = $isMd5
? $dbForPlatform->getDocument('rules', md5($domain))
: $dbForPlatform->findOne('rules', [
Query::equal('domain', [$domain]),
]);
}
if (!$rule->isEmpty()) {
$rule->setAttribute('certificateId', $certificateId);

View file

@ -87,6 +87,7 @@ class Functions extends Action
$events = $payload['events'] ?? [];
$data = $payload['body'] ?? '';
$eventData = $payload['payload'] ?? '';
$platform = $payload['platform'] ?? '';
$function = new Document($payload['function'] ?? []);
$functionId = $payload['functionId'] ?? '';
$user = new Document($payload['user'] ?? []);
@ -166,6 +167,7 @@ class Functions extends Action
'user-agent' => 'Appwrite/' . APP_VERSION_STABLE,
'content-type' => 'application/json'
],
platform: $platform,
data: null,
user: $user,
jwt: null,
@ -206,6 +208,7 @@ class Functions extends Action
path: $path,
method: $method,
headers: $headers,
platform: $platform,
data: $data,
user: $user,
jwt: $jwt,
@ -231,6 +234,7 @@ class Functions extends Action
path: $path,
method: $method,
headers: $headers,
platform: $platform,
data: $data,
user: $user,
jwt: $jwt,
@ -346,6 +350,7 @@ class Functions extends Action
string $path,
string $method,
array $headers,
array $platform,
string $data = null,
?Document $user = null,
string $jwt = null,
@ -486,13 +491,9 @@ class Functions extends Action
$vars[$var->getAttribute('key')] = $var->getAttribute('value', '');
}
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$endpoint = $protocol . '://' . $hostname . "/v1";
// Appwrite vars
$vars = \array_merge($vars, [
'APPWRITE_FUNCTION_API_ENDPOINT' => $endpoint,
'APPWRITE_FUNCTION_API_ENDPOINT' => $platform['endpoint'],
'APPWRITE_FUNCTION_ID' => $functionId,
'APPWRITE_FUNCTION_NAME' => $function->getAttribute('name'),
'APPWRITE_FUNCTION_DEPLOYMENT' => $deploymentId,

View file

@ -71,7 +71,7 @@ class Mails extends Action
$log->addTag('type', empty($smtp) ? 'cloud' : 'smtp');
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') == 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_DOMAIN');
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN');
$recipient = $payload['recipient'];
$subject = $payload['subject'];

View file

@ -52,6 +52,8 @@ class Migrations extends Action
protected array $plan;
protected array $platform;
/**
* @var array<string, int>
*/
@ -106,6 +108,7 @@ 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');
@ -141,6 +144,7 @@ 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 = [];
@ -174,7 +178,7 @@ class Migrations extends Action
),
SourceAppwrite::getName() => new SourceAppwrite(
$credentials['projectId'],
$credentials['endpoint'] === 'http://localhost/v1' ? 'http://appwrite/v1' : $credentials['endpoint'],
$endpoint,
$credentials['apiKey'],
$dataSource,
$database,
@ -205,7 +209,7 @@ class Migrations extends Action
return match ($destination) {
DestinationAppwrite::getName() => new DestinationAppwrite(
$this->project->getId(),
'http://appwrite/v1',
$this->platform['endpoint'],
$apiKey,
$this->dbForProject,
Config::getParam('collections', [])['databases']['collections'],
@ -309,7 +313,7 @@ class Migrations extends Action
) {
$credentials = $migration->getAttribute('credentials', []);
$credentials['projectId'] = $credentials['projectId'] ?? $project->getId();
$credentials['endpoint'] = $credentials['endpoint'] ?? 'http://appwrite/v1';
$credentials['endpoint'] = $credentials['endpoint'] ?? $this->platform['endpoint'];
$credentials['apiKey'] = $credentials['apiKey'] ?? $tempAPIKey;
$migration->setAttribute('credentials', $credentials);
}

View file

@ -9,6 +9,11 @@ use Utopia\System\System;
class Comment
{
public function __construct(
private array $platform
) {
}
// TODO: Add more tips
protected array $tips = [
'Appwrite has crossed the 50K GitHub stars milestone with hundreds of active contributors',
@ -114,7 +119,7 @@ class Comment
$i = 0;
foreach ($projects as $projectId => $project) {
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $this->platform['consoleDomain'] ?? '';
$text .= "## {$project['name']}\n\n";
$text .= "Project ID: `{$projectId}`\n\n";
@ -228,7 +233,7 @@ class Comment
public function generatImage(string $pathLight, string $pathDark, string $alt, int $width): string
{
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
$hostname = System::getEnv('_APP_CONSOLE_DOMAIN', System::getEnv('_APP_DOMAIN', ''));
$hostname = $this->platform['consoleDomain'] ?? '';
$imageLight = $protocol . '://' . $hostname . $pathLight;
$imageDark = $protocol . '://' . $hostname . $pathDark;

View file

@ -200,10 +200,6 @@ class Executor
?int $requestTimeout = null,
string $responseFormat = self::RESPONSE_FORMAT_OBJECT_HEADERS
) {
if (empty($headers['host'])) {
$headers['host'] = System::getEnv('_APP_DOMAIN', '');
}
$runtimeId = "$projectId-$deploymentId";
$route = '/runtimes/' . $runtimeId . '/executions';

View file

@ -75,7 +75,9 @@ class CompressionTest extends Scope
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertArrayHasKey('content-encoding', $response['headers'], 'Content encoding should be gzip, headers received: ' . json_encode($response['headers'], JSON_PRETTY_PRINT));
$this->assertLessThan(2000, intval($response['headers']['content-length']));
$this->assertArrayHasKey('content-length', $response['headers'], 'Compressed response should provide content length, headers received: ' . json_encode($response['headers'], JSON_PRETTY_PRINT));
$this->assertLessThan(2000, \intval($response['headers']['content-length']));
$this->assertArrayNotHasKey('transfer-encoding', $response['headers'], 'Compressed response should not be chunked, headers received: ' . json_encode($response['headers'], JSON_PRETTY_PRINT));
// get prefs without compression
$response = $this->client->call(Client::METHOD_GET, '/users/' . $userId . '/prefs', array_merge([
@ -83,8 +85,15 @@ class CompressionTest extends Scope
], $this->getHeaders()));
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertGreaterThanOrEqual(2000, intval($response['headers']['content-length']));
$this->assertArrayNotHasKey('content-encoding', $response['headers']);
$this->assertEquals('chunked', $response['headers']['transfer-encoding'] ?? null, 'Uncompressed response should use chunked transfer, headers received: ' . json_encode($response['headers'], JSON_PRETTY_PRINT));
$this->assertArrayNotHasKey('content-length', $response['headers'], 'Uncompressed response should not send content length when chunked.');
$this->assertArrayHasKey('longValue', $response['body'], 'Prefs payload should expose longValue at the top level, body received: ' . json_encode($response['body'], JSON_PRETTY_PRINT));
$prefsPayload = $response['body']['longValue'];
$payloadLength = \strlen($prefsPayload);
$this->assertGreaterThanOrEqual(2000, $payloadLength, 'Prefs payload should be at least 2000 bytes.');
}
public function testImageResponse()

View file

@ -15,7 +15,7 @@ class HTTPTest extends Scope
public function setUp(): void
{
parent::setUp();
$this->client->setEndpoint('http://traefik');
$this->client->setEndpoint('http://appwrite.test');
}
public function testOptions()
@ -31,7 +31,7 @@ class HTTPTest extends Scope
$this->assertEquals(204, $response['headers']['status-code']);
$this->assertEquals('Appwrite', $response['headers']['server']);
$this->assertEquals('GET, POST, PUT, PATCH, DELETE', $response['headers']['access-control-allow-methods']);
$this->assertEquals('Origin, Cookie, Set-Cookie, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, X-Appwrite-ID, X-Appwrite-Timestamp, Content-Range, Range, Cache-Control, Expires, Pragma, X-Appwrite-Session, X-Fallback-Cookies, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('Accept, Origin, Cookie, Set-Cookie, Content-Type, Content-Range, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Dev-Key, X-Appwrite-Locale, X-Appwrite-Mode, X-Appwrite-JWT, X-Appwrite-Response-Format, X-Appwrite-Timeout, X-Appwrite-ID, X-Appwrite-Timestamp, X-Appwrite-Session, X-SDK-Version, X-SDK-Name, X-SDK-Language, X-SDK-Platform, X-SDK-GraphQL, Range, Cache-Control, Expires, Pragma, X-Fallback-Cookies, X-Requested-With, X-Forwarded-For, X-Forwarded-User-Agent', $response['headers']['access-control-allow-headers']);
$this->assertEquals('X-Appwrite-Session, X-Fallback-Cookies', $response['headers']['access-control-expose-headers']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertEquals('true', $response['headers']['access-control-allow-credentials']);
@ -66,16 +66,10 @@ class HTTPTest extends Scope
public function testAcmeChallenge()
{
// Preparation
$previousEndpoint = $this->client->getEndpoint();
$this->client->setEndpoint("http://localhost");
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, '/.well-known/acme-challenge/8DdIKX257k6Dih5s_saeVMpTnjPJdKO5Ase0OCiJrIg', \array_merge([
'origin' => 'http://localhost',
]));
$response = $this->client->call(Client::METHOD_GET, '/.well-known/acme-challenge/8DdIKX257k6Dih5s_saeVMpTnjPJdKO5Ase0OCiJrIg');
// 'Unknown path', but validation passed
$this->assertEquals(404, $response['headers']['status-code']);
@ -83,15 +77,10 @@ class HTTPTest extends Scope
/**
* Test for FAILURE
*/
$response = $this->client->call(Client::METHOD_GET, '/.well-known/acme-challenge/../../../../../../../etc/passwd', \array_merge([
'origin' => 'http://localhost',
]));
$response = $this->client->call(Client::METHOD_GET, '/.well-known/acme-challenge/../../../../../../../etc/passwd');
// Check for too many path segments
$this->assertEquals(400, $response['headers']['status-code']);
// Cleanup
$this->client->setEndpoint($previousEndpoint);
// 'Unknown path', but validation passed
$this->assertEquals(404, $response['headers']['status-code']);
}
public function testSpecs()
@ -172,48 +161,30 @@ class HTTPTest extends Scope
public function testCors()
{
/**
* Test for SUCCESS
*/
$endpoint = '/v1/projects'; // Can be any non-404 route
$response = $this->client->call(Client::METHOD_GET, $endpoint);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
/**
* Test for SUCCESS
*/
$response = $this->client->call(Client::METHOD_GET, $endpoint, [
'origin' => 'http://localhost',
]);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$response = $this->client->call(Client::METHOD_GET, $endpoint, [
'origin' => 'http://appwrite.io',
]);
$this->assertEquals('http://appwrite.io', $response['headers']['access-control-allow-origin']);
$response = $this->client->call(Client::METHOD_GET, $endpoint, [
'origin' => 'https://appwrite.io',
]);
$this->assertEquals('https://appwrite.io', $response['headers']['access-control-allow-origin']);
$response = $this->client->call(Client::METHOD_GET, $endpoint, [
'origin' => 'http://cloud.appwrite.io',
]);
$this->assertEquals('http://cloud.appwrite.io', $response['headers']['access-control-allow-origin']);
/**
* Test for FAILURE
*/
// you should not return a fallback origin for a no host
$response = $this->client->call(Client::METHOD_GET, $endpoint);
$this->assertNull($response['headers']['access-control-allow-origin'] ?? null);
// you should not return a fallback origin for a no host
$response = $this->client->call(Client::METHOD_GET, $endpoint, [
'origin' => 'http://google.com',
]);
$this->assertNull($response['headers']['access-control-allow-origin'] ?? null);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
}
public function testConsoleRedirect()

View file

@ -17,7 +17,7 @@ class HooksTest extends Scope
public function setUp(): void
{
parent::setUp();
$this->client->setEndpoint('http://traefik');
$this->client->setEndpoint('http://appwrite.test');
}
public function testProjectHooks()

View file

@ -18,7 +18,7 @@ abstract class Scope extends TestCase
public const REQUEST_TYPE_SMS = 'sms';
protected ?Client $client = null;
protected string $endpoint = 'http://localhost/v1';
protected string $endpoint = 'http://appwrite.test/v1';
protected function setUp(): void
{

View file

@ -50,21 +50,6 @@ trait AccountBase
/**
* Test for FAILURE
*/
// Deny request from blocked IP
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',
'x-appwrite-project' => 'console',
'x-forwarded-for' => '31.6.14.220' // Test IP for denied access region
]), [
'userId' => ID::unique(),
'email' => $email,
'password' => $password,
'name' => $name,
]);
$this->assertEquals(451, $response['headers']['status-code']);
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
'origin' => 'http://localhost',
'content-type' => 'application/json',

View file

@ -2130,10 +2130,15 @@ class AccountCustomClientTest extends Scope
'failure' => 'http://localhost/v1/mock/tests/general/oauth2/failure',
]);
$session = $response['cookies']['a_session_' . $this->getProject()['$id']];
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals('success', $response['body']['result']);
$sessionCookieKey = 'a_session_' . $this->getProject()['$id'];
$this->assertArrayHasKey(
$sessionCookieKey,
$response['cookies'],
"Failed asserting that session cookie '$sessionCookieKey' is set. Cookies: " . json_encode($response['cookies'])
);
$session = $response['cookies'][$sessionCookieKey];
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'origin' => 'http://localhost',

View file

@ -459,7 +459,7 @@ class FunctionsCustomServerTest extends Scope
$this->assertEquals("completed", $execution['body']['status']);
$this->assertEquals(200, $execution['body']['responseStatusCode']);
$this->assertEquals("Pong", $execution['body']['responseBody']);
$this->assertEmpty($execution['body']['errors']);
$this->assertEmpty($execution['body']['errors'], 'Failed to execute function, ' . json_encode($execution['body']['errors']));
// Test execution logged correct total users
$users = $this->client->call(Client::METHOD_GET, '/users', array_merge([

View file

@ -33,7 +33,7 @@ class ScopeTest extends Scope
'x-appwrite-key' => $apiKey,
], $gqlPayload);
$message = "app.{$projectId}@service.localhost (role: applications) missing scopes ([\"databases.write\"])";
$message = "app.{$projectId}@service.appwrite.test (role: applications) missing scopes ([\"databases.write\"])";
$this->assertArrayHasKey('errors', $database['body']);
$this->assertEquals($message, $database['body']['errors'][0]['message']);
}

View file

@ -89,7 +89,7 @@ trait MigrationsBase
{
$response = $this->performMigrationSync([
'resources' => Appwrite::getSupportedResources(),
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -126,7 +126,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_USER,
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -188,7 +188,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_USER,
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -277,7 +277,7 @@ trait MigrationsBase
Resource::TYPE_TEAM,
Resource::TYPE_MEMBERSHIP,
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -393,7 +393,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_DATABASE,
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -484,7 +484,7 @@ trait MigrationsBase
Resource::TYPE_TABLE,
Resource::TYPE_COLUMN,
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -571,7 +571,7 @@ trait MigrationsBase
Resource::TYPE_COLUMN,
Resource::TYPE_ROW,
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -651,7 +651,7 @@ trait MigrationsBase
'resources' => [
Resource::TYPE_BUCKET
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -747,7 +747,7 @@ trait MigrationsBase
Resource::TYPE_BUCKET,
Resource::TYPE_FILE
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -818,7 +818,7 @@ trait MigrationsBase
Resource::TYPE_FUNCTION,
Resource::TYPE_DEPLOYMENT
],
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'projectId' => $this->getProject()['$id'],
'apiKey' => $this->getProject()['apiKey'],
]);
@ -1119,7 +1119,7 @@ trait MigrationsBase
// all data exists, pass.
$migration = $this->performCsvMigration(
[
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'fileId' => $fileIds['default'],
'bucketId' => $bucketIds['default'],
'resourceId' => $databaseId . ':' . $tableId,
@ -1161,7 +1161,7 @@ trait MigrationsBase
// all data exists and includes internals, pass.
$migration = $this->performCsvMigration(
[
'endpoint' => 'http://localhost/v1',
'endpoint' => $this->endpoint,
'fileId' => $fileIds['documents-internals'],
'bucketId' => $bucketIds['documents-internals'],
'resourceId' => $databaseId . ':' . $tableId,

View file

@ -4889,7 +4889,8 @@ class ProjectsConsoleClientTest extends Scope
$this->assertEquals(403, $response['headers']['status-code']);
$this->assertNotEquals($origin, $response['headers']['access-control-allow-origin'] ?? null);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin'] ?? null);
// you should not return a fallback origin for a disallowed host
$this->assertNull($response['headers']['access-control-allow-origin'] ?? null);
/**
@ -4906,7 +4907,7 @@ class ProjectsConsoleClientTest extends Scope
]);
$this->assertEquals(401, $response['headers']['status-code']);
$this->assertEquals('*', $response['headers']['access-control-allow-origin'] ?? null);
$this->assertEquals($origin, $response['headers']['access-control-allow-origin'] ?? null);
}
/**

View file

@ -81,16 +81,11 @@ class ProjectsCustomServerTest extends Scope
$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,
'sites.localhost',
'functions.localhost',
'appwrite.test',
'localhost'
];
foreach ($deniedDomains as $deniedDomain) {

View file

@ -103,12 +103,11 @@ class ProxyCustomServerTest extends Scope
$domain = \uniqid() . '-api.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://' . $domain);
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
// We should ideally assert 400, but server allows unknown domains, and serves API by default
$response = $proxyClient->call(Client::METHOD_GET, '/versions');
$this->assertEquals(200, $response['headers']['status-code']);
$this->assertEquals(APP_VERSION_STABLE, $response['body']['server']);
$this->assertEquals(401, $response['headers']['status-code']);
$ruleId = $this->setupAPIRule($domain);
@ -138,11 +137,11 @@ class ProxyCustomServerTest extends Scope
$domain = \uniqid() . '-redirect.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/todos/1');
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertEquals(401, $response['headers']['status-code']);
$siteId = $this->setupSite()['siteId'];
@ -166,7 +165,7 @@ class ProxyCustomServerTest extends Scope
$this->assertNotEmpty($ruleId);
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/', followRedirects: false);
@ -193,11 +192,11 @@ class ProxyCustomServerTest extends Scope
$domain = \uniqid() . '-function.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/ping');
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertEquals(401, $response['headers']['status-code']);
$setup = $this->setupFunction();
$functionId = $setup['functionId'];
@ -248,11 +247,11 @@ class ProxyCustomServerTest extends Scope
$domain = \uniqid() . '-site.custom.localhost';
$proxyClient = new Client();
$proxyClient->setEndpoint('http://appwrite');
$proxyClient->setEndpoint('http://appwrite.test');
$proxyClient->addHeader('x-appwrite-hostname', $domain);
$response = $proxyClient->call(Client::METHOD_GET, '/contact');
$this->assertEquals(404, $response['headers']['status-code']);
$this->assertEquals(401, $response['headers']['status-code']);
$setup = $this->setupSite();
$siteId = $setup['siteId'];

View file

@ -22,7 +22,7 @@ trait RealtimeBase
];
return new WebSocketClient(
"ws://appwrite-traefik/v1/realtime?" . http_build_query($query),
"ws://appwrite.test/v1/realtime?" . http_build_query($query),
[
"headers" => $headers,
"timeout" => 30,

View file

@ -6,7 +6,6 @@ use Tests\E2E\Client;
use Tests\E2E\Scopes\ProjectCustom;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\SideClient;
use Utopia\System\System;
class SitesCustomClientTest extends Scope
{
@ -122,7 +121,6 @@ class SitesCustomClientTest extends Scope
* Test for SUCCESS
*/
$template = $this->getTemplate('starter-for-react');
$hostname = System::getEnv('_APP_DOMAIN') ?: '';
$this->assertEquals(200, $template['headers']['status-code']);
$this->assertIsArray($template['body']);
$this->assertEquals('starter-for-react', $template['body']['key']);

View file

@ -1962,7 +1962,7 @@ class SitesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'referer' => $url,
'origin' => $url
'origin' => $url,
]));
$this->assertEquals($url, $response['headers']['access-control-allow-origin']);
@ -1971,11 +1971,10 @@ class SitesCustomServerTest extends Scope
'content-type' => 'application/json',
'x-appwrite-project' => 'unknown',
'referer' => $url,
'origin' => $url
'origin' => $url,
]));
$this->assertNotEquals($url, $response['headers']['access-control-allow-origin']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertArrayNotHasKey('access-control-allow-origin', $response['headers']);
$response = $this->client->call(Client::METHOD_GET, '/account', array_merge([
'content-type' => 'application/json',
@ -1984,8 +1983,7 @@ class SitesCustomServerTest extends Scope
'origin' => 'http://unknown.com'
]));
$this->assertNotEquals($url, $response['headers']['access-control-allow-origin']);
$this->assertEquals('http://localhost', $response['headers']['access-control-allow-origin']);
$this->assertArrayNotHasKey('access-control-allow-origin', $response['headers']);
}
public function testSiteDownload(): void

View file

@ -197,7 +197,6 @@ class WebhooksCustomClientTest extends Scope
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire']));
$this->assertEquals($webhook['data']['ip'], '127.0.0.1');
$this->assertNotEmpty($webhook['data']['osCode']);
$this->assertIsString($webhook['data']['osCode']);
$this->assertNotEmpty($webhook['data']['osName']);
@ -286,7 +285,6 @@ class WebhooksCustomClientTest extends Scope
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertIsString($webhook['data']['expire']);
$this->assertEquals($webhook['data']['ip'], '127.0.0.1');
$this->assertNotEmpty($webhook['data']['osCode']);
$this->assertIsString($webhook['data']['osCode']);
$this->assertNotEmpty($webhook['data']['osName']);
@ -372,7 +370,6 @@ class WebhooksCustomClientTest extends Scope
$this->assertNotEmpty($webhook['data']['$id']);
$this->assertNotEmpty($webhook['data']['userId']);
$this->assertEquals(true, (new DatetimeValidator())->isValid($webhook['data']['expire']));
$this->assertEquals($webhook['data']['ip'], '127.0.0.1');
$this->assertNotEmpty($webhook['data']['osCode']);
$this->assertIsString($webhook['data']['osCode']);
$this->assertNotEmpty($webhook['data']['osName']);

View file

@ -0,0 +1,150 @@
<?php
namespace Tests\Unit\Network;
use Appwrite\Network\Cors;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;
final class CorsTest extends TestCase
{
public function testWildcardWithCredentialsThrows(): void
{
$this->expectException(InvalidArgumentException::class);
new Cors(
allowedHosts: ['*'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: true
);
}
public function testWildcardAllowsAnyOrigin(): void
{
$cors = new Cors(
allowedHosts: ['*'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false
);
$result = $cors->headers('https://foo.com');
$this->assertSame('https://foo.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
}
public function testEmptyOriginReturnsStaticHeadersOnly(): void
{
$cors = new Cors(
allowedHosts: ['example.com'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false
);
$result = $cors->headers('');
$this->assertArrayNotHasKey(Cors::HEADER_ALLOW_ORIGIN, $result);
$this->assertSame('false', $result[Cors::HEADER_ALLOW_CREDENTIALS]);
$this->assertSame('GET', $result[Cors::HEADER_ALLOW_METHODS]);
}
public function testInvalidOriginReturnsStaticHeadersOnly(): void
{
$cors = new Cors(
allowedHosts: ['example.com'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false
);
$result = $cors->headers('%%%not-a-url%%%');
$this->assertArrayNotHasKey(Cors::HEADER_ALLOW_ORIGIN, $result);
}
public function testUnlistedOriginReturnsStaticHeadersOnly(): void
{
$cors = new Cors(
allowedHosts: ['allowed.com'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false
);
$result = $cors->headers('https://forbidden.com');
$this->assertArrayNotHasKey(Cors::HEADER_ALLOW_ORIGIN, $result);
}
public function testAllowedOriginIsReturned(): void
{
$cors = new Cors(
allowedHosts: ['example.com'],
allowedMethods: ['POST'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: true
);
$result = $cors->headers('https://example.com');
$this->assertSame('https://example.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
}
public function testOriginIsLowercasedForMatching(): void
{
$cors = new Cors(
allowedHosts: ['example.com'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false
);
$result = $cors->headers('HTTPS://EXAMPLE.COM');
// Lowercase logic is in the class
$this->assertSame('https://example.com', $result[Cors::HEADER_ALLOW_ORIGIN]);
}
public function testHeaderFormatting(): void
{
$cors = new Cors(
allowedHosts: ['example.com'],
allowedMethods: ['GET', 'POST'],
allowedHeaders: ['X-A', 'X-B'],
exposedHeaders: ['E1', 'E2'],
allowCredentials: true
);
$result = $cors->headers('https://example.com');
$this->assertSame('GET, POST', $result[Cors::HEADER_ALLOW_METHODS]);
$this->assertSame('X-A, X-B', $result[Cors::HEADER_ALLOW_HEADERS]);
$this->assertSame('E1, E2', $result[Cors::HEADER_EXPOSE_HEADERS]);
$this->assertSame('true', $result[Cors::HEADER_ALLOW_CREDENTIALS]);
}
public function testMaxAgeIncluded(): void
{
$cors = new Cors(
allowedHosts: ['example.com'],
allowedMethods: ['GET'],
allowedHeaders: ['X-Test'],
exposedHeaders: [],
allowCredentials: false,
maxAge: 999
);
$result = $cors->headers('https://example.com');
$this->assertSame(999, $result[Cors::HEADER_MAX_AGE]);
}
}

View file

@ -2,53 +2,17 @@
namespace Tests\Unit\Network\Validators;
use Appwrite\Network\Platform;
use Appwrite\Network\Validator\Origin;
use PHPUnit\Framework\TestCase;
use Utopia\Database\Helpers\ID;
class OriginTest extends TestCase
{
public function testValues(): void
{
$validator = new Origin([
[
'$collection' => ID::custom('platforms'),
'name' => 'Production',
'type' => Platform::TYPE_WEB,
'hostname' => 'appwrite.io',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Development',
'type' => Platform::TYPE_WEB,
'hostname' => 'appwrite.test',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Localhost',
'type' => Platform::TYPE_WEB,
'hostname' => 'localhost',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Flutter',
'type' => Platform::TYPE_FLUTTER_WEB,
'hostname' => 'appwrite.flutter',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Expo',
'type' => Platform::TYPE_SCHEME,
'key' => 'exp',
],
[
'$collection' => ID::custom('platforms'),
'name' => 'Appwrite Callback',
'type' => Platform::TYPE_SCHEME,
'key' => 'appwrite-callback-123',
],
]);
$validator = new Origin(
allowedHostnames: ['appwrite.io', 'appwrite.test', 'localhost', 'appwrite.flutter'],
allowedSchemes: ['exp', 'appwrite-callback-123']
);
$this->assertEquals(false, $validator->isValid(''));
$this->assertEquals(false, $validator->isValid('/'));