mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
100097eb77 | ||
|
|
cc6ddc5dcd | ||
|
|
a9424a3630 | ||
|
|
5dd9229c2f | ||
|
|
a084d6b5da | ||
|
|
e828e9e82f | ||
|
|
5cea741282 | ||
|
|
5c136b37c0 | ||
|
|
168cbf4409 | ||
|
|
1a7587a175 | ||
|
|
e321712d3a | ||
|
|
d06ee44c49 | ||
|
|
7c96030aca | ||
|
|
8d9ac9d428 | ||
|
|
4b3e6a6422 | ||
|
|
1b6aba4713 | ||
|
|
006f8fef45 | ||
|
|
2c3ddc4e73 | ||
|
|
5642b86c55 | ||
|
|
d3d4b6f67f | ||
|
|
95ffa6bcda | ||
|
|
8aa6d72b3e |
@@ -125,3 +125,4 @@ _APP_WEBHOOK_MAX_FAILED_ATTEMPTS=10
|
||||
_APP_PROJECT_REGIONS=default
|
||||
_APP_FUNCTIONS_CREATION_ABUSE_LIMIT=5000
|
||||
_APP_STATS_USAGE_DUAL_WRITING_DBS=database_db_main
|
||||
_APP_TRUSTED_HEADERS=
|
||||
|
||||
@@ -164,6 +164,7 @@ Getting started with Appwrite is as easy as creating a new project, choosing you
|
||||
| | [Quick start for PHP](https://appwrite.io/docs/quick-starts/php) |
|
||||
| | [Quick start for Kotlin](https://appwrite.io/docs/quick-starts/kotlin) |
|
||||
| | [Quick start for Swift](https://appwrite.io/docs/quick-starts/swift) |
|
||||
| | [Quick start for Go](https://appwrite.io/docs/quick-starts/go) |
|
||||
|
||||
### Products
|
||||
|
||||
@@ -205,6 +206,7 @@ Below is a list of currently supported platforms and languages. If you would lik
|
||||
- :white_check_mark: [Kotlin](https://github.com/appwrite/sdk-for-kotlin) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Swift](https://github.com/appwrite/sdk-for-swift) (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [.NET](https://github.com/appwrite/sdk-for-dotnet) - **Beta** (Maintained by the Appwrite Team)
|
||||
- :white_check_mark: [Go](https://github.com/appwrite/sdk-for-go) - **Beta** (Maintained by the Appwrite Team)
|
||||
|
||||
#### Community
|
||||
|
||||
|
||||
@@ -357,6 +357,15 @@ return [
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
],
|
||||
[
|
||||
'name' => '_APP_TRUSTED_HEADERS',
|
||||
'description' => 'This option allows you to set the list of trusted headers, the value is a comma‑separated list of HTTP header names, evaluated left-to-right for the first valid IP. Header names are treated case-insensitively.',
|
||||
'introduction' => '1.8.0',
|
||||
'default' => 'x-forwarded-for',
|
||||
'required' => false,
|
||||
'question' => '',
|
||||
'filter' => ''
|
||||
]
|
||||
],
|
||||
],
|
||||
|
||||
@@ -194,7 +194,7 @@ function sendSessionAlert(Locale $locale, Document $user, Document $project, Doc
|
||||
}
|
||||
;
|
||||
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode) {
|
||||
$createSession = function (string $userId, string $secret, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Store $store, ProofsToken $proofForToken, ProofsCode $proofForCode, string $trustedIp) {
|
||||
|
||||
/** @var Appwrite\Utopia\Database\Documents\User $userFromRequest */
|
||||
$userFromRequest = Authorization::skip(fn () => $dbForProject->getDocument('users', $userId));
|
||||
@@ -214,7 +214,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$record = $geodb->get($trustedIp);
|
||||
$sessionSecret = $proofForToken->generate();
|
||||
|
||||
$factor = (match ($verifiedToken->getAttribute('type')) {
|
||||
@@ -243,7 +243,7 @@ $createSession = function (string $userId, string $secret, Request $request, Res
|
||||
'provider' => $provider,
|
||||
'secret' => $proofForToken->hash($sessionSecret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
'factors' => [$factor],
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||
@@ -358,13 +358,13 @@ App::post('/v1/account')
|
||||
->param('email', '', new EmailValidator(), 'User email.')
|
||||
->param('password', '', fn ($project, $passwordsDictionary) => new PasswordDictionary($passwordsDictionary, $project->getAttribute('auths', [])['passwordDictionary'] ?? false), 'New user password. Must be between 8 and 256 chars.', false, ['project', 'passwordsDictionary'])
|
||||
->param('name', '', new Text(128), 'User name. Max length: 128 chars.', true)
|
||||
->inject('request')
|
||||
->inject('response')
|
||||
->inject('user')
|
||||
->inject('project')
|
||||
->inject('dbForProject')
|
||||
->inject('hooks')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Request $request, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $userId, string $email, string $password, string $name, Response $response, Document $user, Document $project, Database $dbForProject, Hooks $hooks, string $trustedIp) {
|
||||
|
||||
$email = \strtolower($email);
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -375,7 +375,7 @@ App::post('/v1/account')
|
||||
throw new Exception(Exception::USER_EMAIL_NOT_WHITELISTED);
|
||||
}
|
||||
|
||||
if (!empty($whitelistIPs) && !\in_array($request->getIP(), $whitelistIPs)) {
|
||||
if (!empty($whitelistIPs) && !\in_array($trustedIp, $whitelistIPs)) {
|
||||
throw new Exception(Exception::USER_IP_NOT_WHITELISTED);
|
||||
}
|
||||
}
|
||||
@@ -961,7 +961,8 @@ App::post('/v1/account/sessions/email')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $email, string $password, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Mail $queueForMails, Hooks $hooks, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, string $trustedIp) {
|
||||
$email = \strtolower($email);
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
@@ -985,7 +986,7 @@ App::post('/v1/account/sessions/email')
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$record = $geodb->get($trustedIp);
|
||||
$secret = $proofForToken->generate();
|
||||
$session = new Document(array_merge(
|
||||
[
|
||||
@@ -996,7 +997,7 @@ App::post('/v1/account/sessions/email')
|
||||
'providerUid' => $email,
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
'factors' => ['password'],
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||
@@ -1105,7 +1106,8 @@ App::post('/v1/account/sessions/anonymous')
|
||||
->inject('store')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForToken')
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) {
|
||||
->inject('trustedIp')
|
||||
->action(function (Request $request, Response $response, Locale $locale, User $user, Document $project, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken, string $trustedIp) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
if ('console' === $project->getId()) {
|
||||
@@ -1155,7 +1157,7 @@ App::post('/v1/account/sessions/anonymous')
|
||||
// Create session token
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$record = $geodb->get($trustedIp);
|
||||
$secret = $proofForToken->generate();
|
||||
|
||||
$session = new Document(array_merge(
|
||||
@@ -1166,7 +1168,7 @@ App::post('/v1/account/sessions/anonymous')
|
||||
'provider' => SESSION_PROVIDER_ANONYMOUS,
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
'factors' => ['anonymous'],
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||
@@ -1257,6 +1259,7 @@ App::post('/v1/account/sessions/token')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('trustedIp')
|
||||
->action($createSession);
|
||||
|
||||
App::get('/v1/account/sessions/oauth2/:provider')
|
||||
@@ -1453,7 +1456,8 @@ 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, Validator $redirectValidator, Document $devKey, User $user, Database $dbForProject, Reader $geodb, Event $queueForEvents, Store $store, ProofsPassword $proofForPassword, ProofsToken $proofForToken) use ($oauthDefaultSuccess) {
|
||||
->inject('trustedIp')
|
||||
->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, string $trustedIp) use ($oauthDefaultSuccess) {
|
||||
$protocol = System::getEnv('_APP_OPTIONS_FORCE_HTTPS') === 'disabled' ? 'http' : 'https';
|
||||
$port = $request->getPort();
|
||||
$callbackBase = $protocol . '://' . $request->getHostname();
|
||||
@@ -1816,7 +1820,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
'secret' => $proofForTokenOAuth2->hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
@@ -1840,7 +1844,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
// If the `token` param is not set, we persist the session in a cookie
|
||||
} else {
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$record = $geodb->get($trustedIp);
|
||||
$secret = $proofForToken->generate();
|
||||
|
||||
$session = new Document(array_merge([
|
||||
@@ -1854,7 +1858,7 @@ App::get('/v1/account/sessions/oauth2/:provider/redirect')
|
||||
'providerAccessTokenExpiry' => DateTime::addSeconds(new \DateTime(), (int)$accessTokenExpiry),
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
'factors' => [TYPE::EMAIL, 'oauth2'], // include a special oauth2 factor to bypass MFA checks
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $duration)
|
||||
@@ -2047,7 +2051,8 @@ App::post('/v1/account/tokens/magic-url')
|
||||
->inject('queueForMails')
|
||||
->inject('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) {
|
||||
->inject('trustedIp')
|
||||
->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, string $trustedIp) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2137,7 +2142,7 @@ App::post('/v1/account/tokens/magic-url')
|
||||
'secret' => $proofForToken->hash($tokenSecret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
@@ -2315,7 +2320,8 @@ App::post('/v1/account/tokens/email')
|
||||
->inject('queueForMails')
|
||||
->inject('proofForPassword')
|
||||
->inject('proofForCode')
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $userId, string $email, bool $phrase, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsPassword $proofForPassword, ProofsCode $proofForCode, string $trustedIp) {
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP disabled');
|
||||
}
|
||||
@@ -2421,7 +2427,7 @@ App::post('/v1/account/tokens/email')
|
||||
'secret' => $proofForCode->hash($tokenSecret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
@@ -2607,10 +2613,11 @@ App::put('/v1/account/sessions/magic-url')
|
||||
->inject('queueForMails')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode) use ($createSession) {
|
||||
->inject('trustedIp')
|
||||
->action(function ($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForCode, $trustedIp) use ($createSession) {
|
||||
$proofForToken = new ProofsToken(TOKEN_LENGTH_MAGIC_URL);
|
||||
$proofForToken->setHash(new Sha());
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode);
|
||||
$createSession($userId, $secret, $request, $response, $user, $dbForProject, $project, $locale, $geodb, $queueForEvents, $queueForMails, $store, $proofForToken, $proofForCode, $trustedIp);
|
||||
});
|
||||
|
||||
App::put('/v1/account/sessions/phone')
|
||||
@@ -2655,6 +2662,7 @@ App::put('/v1/account/sessions/phone')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('proofForCode')
|
||||
->inject('trustedIp')
|
||||
->action($createSession);
|
||||
|
||||
App::post('/v1/account/tokens/phone')
|
||||
@@ -2697,7 +2705,8 @@ App::post('/v1/account/tokens/phone')
|
||||
->inject('plan')
|
||||
->inject('store')
|
||||
->inject('proofForCode')
|
||||
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $userId, string $phone, Request $request, Response $response, User $user, Document $project, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, Store $store, ProofsCode $proofForCode, string $trustedIp) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
}
|
||||
@@ -2792,7 +2801,7 @@ App::post('/v1/account/tokens/phone')
|
||||
'secret' => $proofForCode->hash($secret),
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
@@ -3468,7 +3477,8 @@ App::post('/v1/account/recovery')
|
||||
->inject('queueForMails')
|
||||
->inject('queueForEvents')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $email, string $url, Request $request, Response $response, User $user, Database $dbForProject, Document $project, Locale $locale, Mail $queueForMails, Event $queueForEvents, ProofsToken $proofForToken, string $trustedIp) {
|
||||
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
|
||||
@@ -3501,7 +3511,7 @@ App::post('/v1/account/recovery')
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($profile->getId())->toString());
|
||||
@@ -3764,7 +3774,8 @@ App::post('/v1/account/verifications/email')
|
||||
->inject('queueForEvents')
|
||||
->inject('queueForMails')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $url, Request $request, Response $response, Document $project, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $url, Request $request, Response $response, Document $project, User $user, Database $dbForProject, Locale $locale, Event $queueForEvents, Mail $queueForMails, ProofsToken $proofForToken, string $trustedIp) {
|
||||
|
||||
if (empty(System::getEnv('_APP_SMTP_HOST'))) {
|
||||
throw new Exception(Exception::GENERAL_SMTP_DISABLED, 'SMTP Disabled');
|
||||
@@ -3790,7 +3801,7 @@ App::post('/v1/account/verifications/email')
|
||||
'secret' => $proofForToken->hash($verificationSecret), // One way hash encryption to protect DB leak
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
@@ -4052,7 +4063,8 @@ App::post('/v1/account/verifications/phone')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('plan')
|
||||
->inject('proofForCode')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode) {
|
||||
->inject('trustedIp')
|
||||
->action(function (Request $request, Response $response, User $user, Database $dbForProject, Event $queueForEvents, Messaging $queueForMessaging, Document $project, Locale $locale, callable $timelimit, StatsUsage $queueForStatsUsage, array $plan, ProofsCode $proofForCode, string $trustedIp) {
|
||||
if (empty(System::getEnv('_APP_SMS_PROVIDER'))) {
|
||||
throw new Exception(Exception::GENERAL_PHONE_DISABLED, 'Phone provider not configured');
|
||||
}
|
||||
@@ -4088,7 +4100,7 @@ App::post('/v1/account/verifications/phone')
|
||||
'secret' => $proofForCode->hash($secret),
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
]);
|
||||
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
|
||||
@@ -32,11 +32,12 @@ App::get('/v1/locale')
|
||||
->inject('response')
|
||||
->inject('locale')
|
||||
->inject('geodb')
|
||||
->action(function (Request $request, Response $response, Locale $locale, Reader $geodb) {
|
||||
->inject('trustedIp')
|
||||
->action(function (Request $request, Response $response, Locale $locale, Reader $geodb, string $trustedIp) {
|
||||
$eu = Config::getParam('locale-eu');
|
||||
$currencies = Config::getParam('locale-currencies');
|
||||
$output = [];
|
||||
$ip = $request->getIP();
|
||||
$ip = $trustedIp;
|
||||
|
||||
$output['ip'] = $ip;
|
||||
|
||||
|
||||
@@ -1209,7 +1209,8 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $teamId, string $membershipId, string $userId, string $secret, Request $request, Response $response, Document $user, Database $dbForProject, Document $project, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, string $trustedIp) {
|
||||
$protocol = $request->getProtocol();
|
||||
|
||||
$membership = $dbForProject->getDocument('memberships', $membershipId);
|
||||
@@ -1261,7 +1262,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
Authorization::setRole(Role::user($user->getId())->toString());
|
||||
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$record = $geodb->get($trustedIp);
|
||||
$authDuration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::addSeconds(new \DateTime(), $authDuration);
|
||||
$secret = $proofForToken->generate();
|
||||
@@ -1278,7 +1279,7 @@ App::patch('/v1/teams/:teamId/memberships/:membershipId/status')
|
||||
'providerUid' => $user->getAttribute('email'),
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
'factors' => ['email'],
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
'expire' => DateTime::addSeconds(new \DateTime(), $authDuration)
|
||||
|
||||
@@ -2216,7 +2216,8 @@ App::post('/v1/users/:userId/sessions')
|
||||
->inject('queueForEvents')
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $userId, Request $request, Response $response, Database $dbForProject, Document $project, Locale $locale, Reader $geodb, Event $queueForEvents, Store $store, Token $proofForToken, string $trustedIp) {
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
if ($user->isEmpty()) {
|
||||
throw new Exception(Exception::USER_NOT_FOUND);
|
||||
@@ -2224,7 +2225,7 @@ App::post('/v1/users/:userId/sessions')
|
||||
|
||||
$secret = $proofForToken->generate();
|
||||
$detector = new Detector($request->getUserAgent('UNKNOWN'));
|
||||
$record = $geodb->get($request->getIP());
|
||||
$record = $geodb->get($trustedIp);
|
||||
|
||||
$duration = $project->getAttribute('auths', [])['duration'] ?? TOKEN_EXPIRATION_LOGIN_LONG;
|
||||
$expire = DateTime::formatTz(DateTime::addSeconds(new \DateTime(), $duration));
|
||||
@@ -2238,7 +2239,7 @@ App::post('/v1/users/:userId/sessions')
|
||||
'secret' => $proofForToken->hash($secret), // One way hash encryption to protect DB leak
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'factors' => ['server'],
|
||||
'ip' => $request->getIP(),
|
||||
'ip' => $trustedIp,
|
||||
'countryCode' => ($record) ? \strtolower($record['country']['iso_code']) : '--',
|
||||
'expire' => $expire,
|
||||
],
|
||||
@@ -2305,7 +2306,8 @@ App::post('/v1/users/:userId/tokens')
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->inject('trustedIp')
|
||||
->action(function (string $userId, int $length, int $expire, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, string $trustedIp) {
|
||||
$user = $dbForProject->getDocument('users', $userId);
|
||||
|
||||
if ($user->isEmpty()) {
|
||||
@@ -2325,7 +2327,7 @@ App::post('/v1/users/:userId/tokens')
|
||||
'secret' => $proofForToken->hash($secret),
|
||||
'expire' => $expire,
|
||||
'userAgent' => $request->getUserAgent('UNKNOWN'),
|
||||
'ip' => $request->getIP()
|
||||
'ip' => $trustedIp
|
||||
]);
|
||||
|
||||
$token = $dbForProject->createDocument('tokens', $token);
|
||||
|
||||
@@ -366,7 +366,7 @@ function router(App $utopia, Database $dbForPlatform, callable $getProjectDB, Sw
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
$ip = $request->getIP();
|
||||
$ip = $trustedIp;
|
||||
$headers['x-appwrite-client-ip'] = $ip;
|
||||
|
||||
$jwtExpiry = $resource->getAttribute('timeout', 900) + 60; // 1min extra to account for possible cold-starts
|
||||
@@ -1179,8 +1179,9 @@ App::error()
|
||||
->inject('logger')
|
||||
->inject('log')
|
||||
->inject('queueForStatsUsage')
|
||||
->inject('trustedIp')
|
||||
->inject('devKey')
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage) {
|
||||
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, Log $log, StatsUsage $queueForStatsUsage, string $trustedIp) {
|
||||
$version = System::getEnv('_APP_VERSION', 'UNKNOWN');
|
||||
$route = $utopia->getRoute();
|
||||
$class = \get_class($error);
|
||||
@@ -1291,7 +1292,7 @@ App::error()
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
} else {
|
||||
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
|
||||
$log->setUser(new User('guest-' . hash('sha256', $trustedIp)));
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -509,7 +509,8 @@ App::init()
|
||||
->inject('devKey')
|
||||
->inject('telemetry')
|
||||
->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) {
|
||||
->inject('trustedIp')
|
||||
->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, string $trustedIp) use ($usageDatabaseListener, $eventDatabaseListener) {
|
||||
|
||||
$route = $utopia->getRoute();
|
||||
|
||||
@@ -537,7 +538,7 @@ App::init()
|
||||
->setParam('{projectId}', $project->getId())
|
||||
->setParam('{userId}', $user->getId())
|
||||
->setParam('{userAgent}', $request->getUserAgent(''))
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{ip}', $trustedIp)
|
||||
->setParam('{url}', $request->getHostname() . $route->getPath())
|
||||
->setParam('{method}', $request->getMethod())
|
||||
->setParam('{chunkId}', (int)($start / ($end + 1 - $start)));
|
||||
@@ -598,7 +599,7 @@ App::init()
|
||||
$queueForAudits
|
||||
->setMode($mode)
|
||||
->setUserAgent($request->getUserAgent(''))
|
||||
->setIP($request->getIP())
|
||||
->setIP($trustedIp)
|
||||
->setHostname($request->getHostname())
|
||||
->setEvent($route->getLabel('audits.event', ''))
|
||||
->setProject($project);
|
||||
|
||||
@@ -36,11 +36,12 @@ App::init()
|
||||
->inject('request')
|
||||
->inject('project')
|
||||
->inject('geodb')
|
||||
->action(function (App $utopia, Request $request, Document $project, Reader $geodb) {
|
||||
->inject('trustedIp')
|
||||
->action(function (App $utopia, Request $request, Document $project, Reader $geodb, string $trustedIp) {
|
||||
$denylist = System::getEnv('_APP_CONSOLE_COUNTRIES_DENYLIST', '');
|
||||
if (!empty($denylist && $project->getId() === 'console')) {
|
||||
$countries = explode(',', $denylist);
|
||||
$record = $geodb->get($request->getIP()) ?? [];
|
||||
$record = $geodb->get($trustedIp) ?? [];
|
||||
$country = $record['country']['iso_code'] ?? '';
|
||||
if (in_array($country, $countries)) {
|
||||
throw new Exception(Exception::GENERAL_REGION_ACCESS_DENIED);
|
||||
|
||||
+2
-1
@@ -2,6 +2,7 @@
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Apppwrite\Network\TrustedIp;
|
||||
use Appwrite\Utopia\Request;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Swoole\Constant;
|
||||
@@ -477,7 +478,7 @@ $http->on(Constant::EVENT_REQUEST, function (SwooleRequest $swooleRequest, Swool
|
||||
if (isset($user) && !$user->isEmpty()) {
|
||||
$log->setUser(new User($user->getId()));
|
||||
} else {
|
||||
$log->setUser(new User('guest-' . hash('sha256', $request->getIP())));
|
||||
$log->setUser(new User('guest-' . hash('sha256', TrustedIp::extract($request, System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for')))));
|
||||
}
|
||||
|
||||
$log->setNamespace("http");
|
||||
|
||||
@@ -22,6 +22,7 @@ use Appwrite\Extend\Exception;
|
||||
use Appwrite\GraphQL\Schema;
|
||||
use Appwrite\Network\Cors;
|
||||
use Appwrite\Network\Platform;
|
||||
use Appwrite\Network\TrustedIp;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Network\Validator\Redirect;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
@@ -859,6 +860,10 @@ App::setResource('servers', function () {
|
||||
return $languages;
|
||||
});
|
||||
|
||||
App::setResource('trustedIp', function (Request $request) {
|
||||
return TrustedIp::extract($request, System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for'));
|
||||
}, ['request']);
|
||||
|
||||
App::setResource('promiseAdapter', function ($register) {
|
||||
return $register->get('promiseAdapter');
|
||||
}, ['register']);
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@
|
||||
use Appwrite\Extend\Exception;
|
||||
use Appwrite\Extend\Exception as AppwriteException;
|
||||
use Appwrite\Messaging\Adapter\Realtime;
|
||||
use Appwrite\Network\Validator\Origin;
|
||||
use Appwrite\Network\TrustedIp;
|
||||
use Appwrite\PubSub\Adapter\Pool as PubSubPool;
|
||||
use Appwrite\Utopia\Database\Documents\User;
|
||||
use Appwrite\Utopia\Request;
|
||||
@@ -552,7 +552,7 @@ $server->onOpen(function (int $connection, SwooleRequest $request) use ($server,
|
||||
*/
|
||||
$timelimit = $timelimit('url:{url},ip:{ip}', 128, 60);
|
||||
$timelimit
|
||||
->setParam('{ip}', $request->getIP())
|
||||
->setParam('{ip}', TrustedIp::extract($request, System::getEnv('_APP_TRUSTED_HEADERS', 'x-forwarded-for')))
|
||||
->setParam('{url}', $request->getURI());
|
||||
|
||||
$abuse = new Abuse($timelimit);
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace Appwrite\Network;
|
||||
|
||||
use Appwrite\Utopia\Request;
|
||||
|
||||
class TrustedIp
|
||||
{
|
||||
/**
|
||||
* Extract the trusted client IP address from a request.
|
||||
*
|
||||
* This method checks configured trusted headers (e.g., X-Forwarded-For)
|
||||
* and falls back to the remote address if no valid IP is found.
|
||||
*
|
||||
* @param Request $request The Utopia request object
|
||||
* @return string The trusted client IP address
|
||||
*
|
||||
*/
|
||||
public static function extract(Request $request, string $trustedHeaders): string
|
||||
{
|
||||
// Fallback to remote address
|
||||
$remoteAddr = $request->getServer('remote_addr') ?? '0.0.0.0';
|
||||
|
||||
$trustedHeaders = explode(',', $trustedHeaders);
|
||||
$trustedHeaders = array_map('trim', $trustedHeaders);
|
||||
$trustedHeaders = array_map('strtolower', $trustedHeaders);
|
||||
$trustedHeaders = array_filter($trustedHeaders);
|
||||
|
||||
foreach ($trustedHeaders as $header) {
|
||||
$headerValue = $request->getHeader($header);
|
||||
|
||||
if (empty($headerValue)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Leftmost IP address is the address of the originating client
|
||||
$ips = explode(',', $headerValue);
|
||||
$ip = trim($ips[0]);
|
||||
|
||||
// Validate IP format (supports both IPv4 and IPv6)
|
||||
if (filter_var($ip, FILTER_VALIDATE_IP)) {
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
|
||||
return $remoteAddr;
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ class Create extends Base
|
||||
->inject('store')
|
||||
->inject('proofForToken')
|
||||
->inject('executor')
|
||||
->inject('trustedIp')
|
||||
->inject('platform')
|
||||
->callback($this->action(...));
|
||||
}
|
||||
@@ -123,6 +124,7 @@ class Create extends Base
|
||||
Store $store,
|
||||
Token $proofForToken,
|
||||
Executor $executor,
|
||||
string $trustedIp,
|
||||
array $platform
|
||||
) {
|
||||
$async = \strval($async) === 'true' || \strval($async) === '1';
|
||||
@@ -238,7 +240,7 @@ class Create extends Base
|
||||
$headers['x-appwrite-country-code'] = '';
|
||||
$headers['x-appwrite-continent-code'] = '';
|
||||
$headers['x-appwrite-continent-eu'] = 'false';
|
||||
$ip = $request->getIP();
|
||||
$ip = $trustedIp;
|
||||
$headers['x-appwrite-client-ip'] = $ip;
|
||||
|
||||
if (!empty($ip)) {
|
||||
|
||||
@@ -326,4 +326,38 @@ trait AccountBase
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 204);
|
||||
}
|
||||
|
||||
public function testTrustedIp(): void
|
||||
{
|
||||
$email = uniqid() . 'user@localhost.test';
|
||||
$password = 'password';
|
||||
$name = 'User Name';
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-forwarded-for' => '203.0.113.195',
|
||||
]), [
|
||||
'userId' => ID::unique(),
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
'name' => $name,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
|
||||
$response = $this->client->call(Client::METHOD_POST, '/account/sessions/email', array_merge([
|
||||
'origin' => 'http://localhost',
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-forwarded-for' => '203.0.113.195',
|
||||
]), [
|
||||
'email' => $email,
|
||||
'password' => $password,
|
||||
]);
|
||||
|
||||
$this->assertEquals($response['headers']['status-code'], 201);
|
||||
$this->assertEquals('203.0.113.195', $response['body']['clientIp'] ?? $response['body']['ip'] ?? '');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user