1
0
Fork 0
mirror of synced 2024-07-05 06:31:08 +12:00
appwrite/app/controllers/general.php

860 lines
35 KiB
PHP
Raw Normal View History

2019-05-09 18:54:39 +12:00
<?php
2022-05-24 02:54:50 +12:00
require_once __DIR__ . '/../init.php';
2019-10-25 06:53:37 +13:00
2019-05-09 18:54:39 +12:00
use Utopia\App;
2022-12-15 05:04:06 +13:00
use Utopia\Database\Helpers\Role;
2022-05-27 02:46:08 +12:00
use Utopia\Locale\Locale;
use Utopia\Logger\Logger;
use Utopia\Logger\Log;
use Utopia\Logger\Log\User;
use Swoole\Http\Request as SwooleRequest;
use Utopia\Cache\Cache;
use Utopia\Pools\Group;
use Appwrite\Utopia\Request;
2020-06-29 19:22:53 +12:00
use Appwrite\Utopia\Response;
2021-12-14 22:11:34 +13:00
use Appwrite\Utopia\View;
2022-05-27 22:36:06 +12:00
use Appwrite\Extend\Exception as AppwriteException;
2020-03-29 01:42:16 +13:00
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Event\Certificate;
2020-06-12 07:36:10 +12:00
use Appwrite\Network\Validator\Origin;
2022-03-01 09:55:18 +13:00
use Appwrite\Utopia\Response\Filters\V11 as ResponseV11;
2022-03-01 09:54:20 +13:00
use Appwrite\Utopia\Response\Filters\V12 as ResponseV12;
use Appwrite\Utopia\Response\Filters\V13 as ResponseV13;
2022-06-28 21:15:27 +12:00
use Appwrite\Utopia\Response\Filters\V14 as ResponseV14;
use Appwrite\Utopia\Response\Filters\V15 as ResponseV15;
2023-08-23 06:13:37 +12:00
use Appwrite\Utopia\Response\Filters\V16 as ResponseV16;
2020-10-30 11:04:53 +13:00
use Utopia\CLI\Console;
2022-05-27 02:46:08 +12:00
use Utopia\Database\Database;
use Utopia\Database\DateTime;
2021-05-16 21:18:34 +12:00
use Utopia\Database\Document;
use Utopia\Database\Query;
2021-07-26 02:51:04 +12:00
use Utopia\Database\Validator\Authorization;
2022-04-20 21:31:17 +12:00
use Utopia\Validator\Hostname;
2022-03-01 09:55:18 +13:00
use Appwrite\Utopia\Request\Filters\V12 as RequestV12;
use Appwrite\Utopia\Request\Filters\V13 as RequestV13;
use Appwrite\Utopia\Request\Filters\V14 as RequestV14;
2022-08-18 01:08:09 +12:00
use Appwrite\Utopia\Request\Filters\V15 as RequestV15;
2023-08-12 12:41:48 +12:00
use Appwrite\Utopia\Request\Filters\V16 as RequestV16;
use Utopia\Validator\Text;
2022-08-06 00:08:04 +12:00
use Utopia\Validator\WhiteList;
2019-05-09 18:54:39 +12:00
2020-06-29 08:45:36 +12:00
Config::setParam('domainVerification', false);
2020-07-01 18:35:57 +12:00
Config::setParam('cookieDomain', 'localhost');
Config::setParam('cookieSamesite', Response::COOKIE_SAMESITE_NONE);
2020-06-30 09:43:34 +12:00
2023-07-28 19:56:07 +12:00
function router(App $utopia, Database $dbForConsole, SwooleRequest $swooleRequest, Request $request, Response $response)
2023-03-15 08:31:23 +13:00
{
2023-10-03 03:02:48 +13:00
$utopia->getRoute()?->label('error', __DIR__ . '/../views/general/error.phtml');
2023-07-28 19:56:07 +12:00
$host = $request->getHostname() ?? '';
2023-03-14 02:35:34 +13:00
2023-02-28 01:43:20 +13:00
$route = Authorization::skip(
2023-08-07 01:11:30 +12:00
fn () => $dbForConsole->find('rules', [
2023-02-28 01:43:20 +13:00
Query::equal('domain', [$host]),
Query::limit(1)
])
)[0] ?? null;
2023-03-15 08:31:23 +13:00
if ($route === null) {
2023-10-03 03:02:48 +13:00
if ($host === App::getEnv('_APP_DOMAIN_FUNCTIONS', '')) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain cannot be used for security reasons. Please use any subdomain instead.');
}
if (\str_ends_with($host, App::getEnv('_APP_DOMAIN_FUNCTIONS', ''))) {
throw new AppwriteException(AppwriteException::GENERAL_ACCESS_FORBIDDEN, 'This domain is not connected to any Appwrite resource yet. Please configure custom domain or function domain to allow this request.');
}
if (App::getEnv('_APP_OPTIONS_ROUTER_PROTECTION', 'disabled') === 'enabled') {
if ($host !== 'localhost' && $host !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
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.');
}
}
// Act as API - no Proxy logic
2023-10-03 03:02:48 +13:00
$utopia->getRoute()?->label('error', '');
return false;
2023-02-28 01:43:20 +13:00
}
2023-03-11 01:20:24 +13:00
$projectId = $route->getAttribute('projectId');
$project = Authorization::skip(
2023-08-07 01:11:30 +12:00
fn () => $dbForConsole->getDocument('projects', $projectId)
2023-03-11 01:20:24 +13:00
);
2023-03-15 08:31:23 +13:00
if (array_key_exists('proxy', $project->getAttribute('services', []))) {
2023-03-11 01:20:24 +13:00
$status = $project->getAttribute('services', [])['proxy'];
2023-03-15 08:31:23 +13:00
if (!$status) {
2023-03-11 01:20:24 +13:00
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
}
}
2023-03-14 02:35:34 +13:00
// Skip Appwrite Router for ACME challenge. Nessessary for certificate generation
2023-08-07 01:11:30 +12:00
$path = ($swooleRequest->server['request_uri'] ?? '/');
2023-03-15 08:31:23 +13:00
if (\str_starts_with($path, '/.well-known/acme-challenge')) {
2023-03-14 02:35:34 +13:00
return false;
}
2023-02-28 01:43:20 +13:00
$type = $route->getAttribute('resourceType');
2023-03-15 08:31:23 +13:00
if ($type === 'function') {
2023-10-03 03:02:48 +13:00
if (App::getEnv('_APP_OPTIONS_FUNCTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
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.');
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
}
}
2023-02-28 01:43:20 +13:00
$functionId = $route->getAttribute('resourceId');
$projectId = $route->getAttribute('projectId');
2023-08-07 01:11:30 +12:00
$path = ($swooleRequest->server['request_uri'] ?? '/');
2023-02-28 01:43:20 +13:00
$query = ($swooleRequest->server['query_string'] ?? '');
2023-03-15 08:31:23 +13:00
if (!empty($query)) {
2023-02-28 01:43:20 +13:00
$path .= '?' . $query;
}
2023-10-28 04:25:19 +13:00
$requestHeaders = $request->getHeaders();
2023-10-27 21:26:46 +13:00
2023-02-28 01:43:20 +13:00
$body = \json_encode([
'async' => false,
'body' => $swooleRequest->getContent() ?? '',
'method' => $swooleRequest->server['request_method'],
'path' => $path,
2023-10-28 04:25:19 +13:00
'headers' => $requestHeaders
2023-02-28 01:43:20 +13:00
]);
$headers = [
'Content-Type: application/json',
'Content-Length: ' . \strlen($body),
2023-08-23 05:32:19 +12:00
'X-Appwrite-Project: ' . $projectId
2023-02-28 01:43:20 +13:00
];
$ch = \curl_init();
\curl_setopt($ch, CURLOPT_URL, "http://localhost/v1/functions/{$functionId}/executions");
\curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
\curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
\curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
\curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
// \curl_setopt($ch, CURLOPT_HEADER, true);
\curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
2023-10-01 04:24:09 +13:00
\curl_setopt($ch, CURLOPT_TIMEOUT, 30);
2023-02-28 01:43:20 +13:00
$executionResponse = \curl_exec($ch);
$statusCode = \curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = \curl_error($ch);
$errNo = \curl_errno($ch);
\curl_close($ch);
if ($errNo !== 0) {
2023-07-11 20:53:40 +12:00
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Internal error: " . $error);
2023-02-28 01:43:20 +13:00
}
if ($statusCode >= 400) {
$error = \json_decode($executionResponse, true)['message'];
2023-07-11 20:53:40 +12:00
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, "Execution error: " . $error);
2023-02-28 01:43:20 +13:00
}
$execution = \json_decode($executionResponse, true);
2023-08-07 01:42:18 +12:00
$contentType = 'text/plain';
2023-07-31 01:12:55 +12:00
foreach ($execution['responseHeaders'] as $header) {
2023-08-07 01:42:18 +12:00
if (\strtolower($header['name']) === 'content-type') {
$contentType = $header['value'];
}
2023-08-07 01:11:30 +12:00
$response->setHeader($header['name'], $header['value']);
2023-02-28 01:43:20 +13:00
}
2023-07-31 01:12:55 +12:00
$body = $execution['responseBody'] ?? '';
2023-02-28 01:43:20 +13:00
2023-08-07 01:11:30 +12:00
$encodingKey = \array_search('x-open-runtimes-encoding', \array_column($execution['responseHeaders'], 'name'));
2023-08-11 21:19:05 +12:00
if ($encodingKey !== false) {
if (($execution['responseHeaders'][$encodingKey]['value'] ?? '') === 'base64') {
2023-08-07 01:11:30 +12:00
$body = \base64_decode($body);
}
2023-02-28 01:43:20 +13:00
}
2023-08-07 01:42:18 +12:00
$response
->setContentType($contentType)
->setStatusCode($execution['responseStatusCode'] ?? 200)
->send($body);
2023-02-28 01:43:20 +13:00
return true;
2023-03-15 08:31:23 +13:00
} elseif ($type === 'api') {
2023-09-06 19:34:02 +12:00
$utopia->getRoute()?->label('error', '');
2023-03-11 01:20:24 +13:00
return false;
2023-02-28 01:43:20 +13:00
} else {
2023-07-28 20:27:16 +12:00
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Unknown resource type ' . $type);
2023-02-28 01:43:20 +13:00
}
2023-10-03 03:02:48 +13:00
$utopia->getRoute()?->label('error', '');
2023-02-28 01:43:20 +13:00
return false;
}
2022-07-22 18:00:42 +12:00
App::init()
2023-08-07 01:42:18 +12:00
->groups(['api', 'web'])
2022-07-22 18:00:42 +12:00
->inject('utopia')
->inject('swooleRequest')
2022-07-22 18:00:42 +12:00
->inject('request')
->inject('response')
->inject('console')
->inject('project')
->inject('dbForConsole')
->inject('user')
->inject('locale')
2023-04-17 14:10:17 +12:00
->inject('localeCodes')
2022-07-22 18:00:42 +12:00
->inject('clients')
2022-08-11 01:45:04 +12:00
->inject('servers')
2022-12-21 05:11:30 +13:00
->inject('queueForCertificates')
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Document $console, Document $project, Database $dbForConsole, Document $user, Locale $locale, array $localeCodes, array $clients, array $servers, Certificate $queueForCertificates) {
/*
* Appwrite Router
*/
2023-07-31 01:30:30 +12:00
2023-07-28 19:56:07 +12:00
$host = $request->getHostname() ?? '';
2023-02-23 04:07:34 +13:00
$mainDomain = App::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
2023-07-28 19:56:07 +12:00
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
2023-02-28 01:43:20 +13:00
return;
2023-02-21 23:30:16 +13:00
}
}
2023-02-21 23:31:55 +13:00
2022-07-22 18:00:42 +12:00
/*
* Request format
*/
2023-02-20 00:04:12 +13:00
$route = $utopia->getRoute();
2022-07-22 18:00:42 +12:00
Request::setRoute($route);
2023-02-21 23:31:55 +13:00
if ($route === null) {
2023-08-18 18:55:44 +12:00
return $response->setStatusCode(404)->send('Not Found');
2023-02-15 02:58:13 +13:00
}
2022-07-22 18:00:42 +12:00
$requestFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($requestFormat) {
switch ($requestFormat) {
case version_compare($requestFormat, '0.12.0', '<'):
Request::setFilter(new RequestV12());
break;
case version_compare($requestFormat, '0.13.0', '<'):
Request::setFilter(new RequestV13());
break;
case version_compare($requestFormat, '0.14.0', '<'):
Request::setFilter(new RequestV14());
break;
case version_compare($requestFormat, '0.15.3', '<'):
2022-08-18 01:08:09 +12:00
Request::setFilter(new RequestV15());
break;
2023-08-12 12:41:48 +12:00
case version_compare($requestFormat, '1.4.0', '<'):
Request::setFilter(new RequestV16());
break;
2022-07-22 18:00:42 +12:00
default:
Request::setFilter(null);
}
} else {
2022-07-22 18:00:42 +12:00
Request::setFilter(null);
}
2022-07-22 18:00:42 +12:00
$domain = $request->getHostname();
$domains = Config::getParam('domains', []);
if (!array_key_exists($domain, $domains)) {
$domain = new Domain(!empty($domain) ? $domain : '');
2022-07-22 18:00:42 +12:00
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.');
2022-03-29 23:17:56 +13:00
} else {
2022-07-22 18:00:42 +12:00
Authorization::disable();
$envDomain = App::getEnv('_APP_DOMAIN', '');
$mainDomain = null;
if (!empty($envDomain) && $envDomain !== 'localhost') {
$mainDomain = $envDomain;
} else {
2023-09-06 20:27:21 +12:00
$domainDocument = $dbForConsole->findOne('rules', [Query::orderAsc('$id')]);
2022-07-22 18:00:42 +12:00
$mainDomain = $domainDocument ? $domainDocument->getAttribute('domain') : $domain->get();
}
2022-07-22 18:00:42 +12:00
if ($mainDomain !== $domain->get()) {
Console::warning($domain->get() . ' is not a main domain. Skipping SSL certificate generation.');
} else {
2023-09-06 03:09:54 +12:00
$domainDocument = $dbForConsole->findOne('rules', [
2022-08-12 11:53:52 +12:00
Query::equal('domain', [$domain->get()])
2022-03-29 23:17:56 +13:00
]);
2022-07-22 18:00:42 +12:00
if (!$domainDocument) {
$domainDocument = new Document([
'domain' => $domain->get(),
2023-09-06 03:09:54 +12:00
'resourceType' => 'api',
'status' => 'verifying',
'projectId' => 'console',
2023-09-06 20:31:40 +12:00
'projectInternalId' => 'console'
2022-07-22 18:00:42 +12:00
]);
2023-09-06 03:09:54 +12:00
$domainDocument = $dbForConsole->createDocument('rules', $domainDocument);
2022-07-22 18:00:42 +12:00
Console::info('Issuing a TLS certificate for the main domain (' . $domain->get() . ') in a few seconds...');
2022-12-14 00:16:12 +13:00
$queueForCertificates
2022-07-22 18:00:42 +12:00
->setDomain($domainDocument)
2023-09-06 03:09:54 +12:00
->setSkipRenewCheck(true)
2022-07-22 18:00:42 +12:00
->trigger();
}
2022-05-12 22:56:25 +12:00
}
2022-07-22 18:00:42 +12:00
$domains[$domain->get()] = true;
Authorization::reset(); // ensure authorization is re-enabled
}
2022-07-22 18:00:42 +12:00
Config::setParam('domains', $domains);
}
2022-07-22 18:00:42 +12:00
$localeParam = (string) $request->getParam('locale', $request->getHeader('x-appwrite-locale', ''));
2023-04-17 14:10:17 +12:00
if (\in_array($localeParam, $localeCodes)) {
2022-07-22 18:00:42 +12:00
$locale->setDefault($localeParam);
}
2022-07-22 18:00:42 +12:00
if ($project->isEmpty()) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
2021-01-03 04:35:21 +13:00
}
2022-07-22 18:00:42 +12:00
if (!empty($route->getLabel('sdk.auth', [])) && $project->isEmpty() && ($route->getLabel('scope', '') !== 'public')) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::PROJECT_UNKNOWN);
2020-06-02 07:58:58 +12:00
}
2022-07-22 18:00:42 +12:00
$referrer = $request->getReferer();
$origin = \parse_url($request->getOrigin($referrer), PHP_URL_HOST);
$protocol = \parse_url($request->getOrigin($referrer), PHP_URL_SCHEME);
$port = \parse_url($request->getOrigin($referrer), PHP_URL_PORT);
$refDomainOrigin = 'localhost';
$validator = new Hostname($clients);
if ($validator->isValid($origin)) {
$refDomainOrigin = $origin;
}
$refDomain = (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $refDomainOrigin . (!empty($port) ? ':' . $port : '');
$refDomain = (!$route->getLabel('origin', false)) // This route is publicly accessible
? $refDomain
: (!empty($protocol) ? $protocol : $request->getProtocol()) . '://' . $origin . (!empty($port) ? ':' . $port : '');
$selfDomain = new Domain($request->getHostname());
$endDomain = new Domain((string)$origin);
Config::setParam(
'domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
2023-08-07 01:11:30 +12:00
$endDomain->getRegisterable() !== ''
2022-07-22 18:00:42 +12:00
);
2023-07-21 22:08:34 +12:00
$isLocalHost = $request->getHostname() === 'localhost' || $request->getHostname() === 'localhost:' . $request->getPort();
$isIpAddress = filter_var($request->getHostname(), FILTER_VALIDATE_IP) !== false;
$isConsoleProject = $project->getAttribute('$id', '') === 'console';
$isConsoleRootSession = App::getEnv('_APP_CONSOLE_ROOT_SESSION', 'disabled') === 'enabled';
Config::setParam(
'cookieDomain',
$isLocalHost || $isIpAddress
? null
: ($isConsoleProject && $isConsoleRootSession
? '.' . $selfDomain->getRegisterable()
: '.' . $request->getHostname()
)
);
2022-07-22 18:00:42 +12:00
/*
* Response format
*/
$responseFormat = $request->getHeader('x-appwrite-response-format', App::getEnv('_APP_SYSTEM_RESPONSE_FORMAT', ''));
if ($responseFormat) {
switch ($responseFormat) {
case version_compare($responseFormat, '0.11.2', '<='):
Response::setFilter(new ResponseV11());
break;
case version_compare($responseFormat, '0.12.4', '<='):
Response::setFilter(new ResponseV12());
2019-10-25 06:53:37 +13:00
break;
2022-07-22 18:00:42 +12:00
case version_compare($responseFormat, '0.13.4', '<='):
Response::setFilter(new ResponseV13());
2019-10-25 06:53:37 +13:00
break;
2022-07-22 18:00:42 +12:00
case version_compare($responseFormat, '0.14.0', '<='):
Response::setFilter(new ResponseV14());
2019-10-25 06:53:37 +13:00
break;
case version_compare($responseFormat, '0.15.3', '<='):
Response::setFilter(new ResponseV15());
break;
2023-08-15 07:53:20 +12:00
case version_compare($responseFormat, '1.4.0', '<'):
Response::setFilter(new ResponseV16());
break;
2022-07-22 18:00:42 +12:00
default:
Response::setFilter(null);
2019-10-25 06:53:37 +13:00
}
2022-07-22 18:00:42 +12:00
} else {
Response::setFilter(null);
2019-10-25 06:53:37 +13:00
}
2021-08-18 22:20:49 +12:00
2020-12-29 06:03:47 +13:00
/*
2022-07-22 18:00:42 +12:00
* Security Headers
*
* As recommended at:
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
if (App::getEnv('_APP_OPTIONS_FORCE_HTTPS', 'disabled') === 'enabled') { // Force HTTPS
if ($request->getProtocol() !== 'https' && ($swooleRequest->header['host'] ?? '') !== 'localhost' && ($swooleRequest->header['host'] ?? '') !== APP_HOSTNAME_INTERNAL) { // localhost allowed for proxy, APP_HOSTNAME_INTERNAL allowed for migrations
2022-07-22 18:00:42 +12:00
if ($request->getMethod() !== Request::METHOD_GET) {
2023-10-03 03:02:48 +13:00
throw new AppwriteException(AppwriteException::GENERAL_PROTOCOL_UNSUPPORTED, 'Method unsupported over HTTP. Please use HTTPS instead.');
2022-07-22 18:00:42 +12:00
}
return $response->redirect('https://' . $request->getHostname() . $request->getURI());
2022-06-03 21:12:19 +12:00
}
2022-11-21 16:49:45 +13:00
}
2022-06-01 03:41:12 +12:00
2022-11-21 16:49:45 +13:00
if ($request->getProtocol() === 'https') {
2022-07-22 18:00:42 +12:00
$response->addHeader('Strict-Transport-Security', 'max-age=' . (60 * 60 * 24 * 126)); // 126 days
2020-12-29 06:03:47 +13:00
}
2019-05-09 18:54:39 +12:00
2022-07-22 18:00:42 +12:00
$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-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')
2022-07-22 18:00:42 +12:00
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $refDomain)
2023-08-07 01:11:30 +12:00
->addHeader('Access-Control-Allow-Credentials', 'true');
2019-05-09 18:54:39 +12:00
2022-07-22 18:00:42 +12:00
/*
* 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(\array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
2019-05-09 18:54:39 +12:00
2022-05-31 23:35:59 +12:00
if (
2022-07-22 18:00:42 +12:00
!$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', ''))
2022-05-31 23:35:59 +12:00
) {
2022-08-14 18:56:12 +12:00
throw new AppwriteException(AppwriteException::GENERAL_UNKNOWN_ORIGIN, $originValidator->getDescription());
2021-07-29 22:28:17 +12:00
}
2019-05-09 18:54:39 +12:00
2022-07-22 18:00:42 +12:00
/*
* ACL Check
*/
2022-08-19 16:05:00 +12:00
$role = ($user->isEmpty())
? Role::guests()->toString()
2022-08-19 16:04:33 +12:00
: Role::users()->toString();
2022-07-22 18:00:42 +12:00
// Add user roles
$memberships = $user->find('teamId', $project->getAttribute('teamId'), 'memberships');
2022-07-22 18:00:42 +12:00
if ($memberships) {
foreach ($memberships->getAttribute('roles', []) as $memberRole) {
switch ($memberRole) {
case 'owner':
$role = Auth::USER_ROLE_OWNER;
break;
case 'admin':
$role = Auth::USER_ROLE_ADMIN;
break;
case 'developer':
$role = Auth::USER_ROLE_DEVELOPER;
break;
}
}
}
2021-08-18 22:20:49 +12:00
2022-07-22 18:00:42 +12:00
$roles = Config::getParam('roles', []);
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
$authKey = $request->getHeader('x-appwrite-key', '');
if (!empty($authKey)) { // API Key authentication
// Check if given key match project API keys
$key = $project->find('secret', $authKey, 'keys');
/*
* Try app auth when we have project key and no user
* Mock user to app and grant API key scopes in addition to default app scopes
*/
if ($key && $user->isEmpty()) {
$user = new Document([
'$id' => '',
'status' => true,
'email' => 'app.' . $project->getId() . '@service.' . $request->getHostname(),
'password' => '',
'name' => $project->getAttribute('name', 'Untitled'),
]);
2022-08-15 19:20:10 +12:00
$role = Auth::USER_ROLE_APPS;
2022-07-22 18:00:42 +12:00
$scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
$expire = $key->getAttribute('expire');
2022-08-16 05:37:26 +12:00
if (!empty($expire) && $expire < DateTime::formatTz(DateTime::now())) {
2023-08-07 01:11:30 +12:00
throw new AppwriteException(AppwriteException::PROJECT_KEY_EXPIRED);
2022-07-22 18:00:42 +12:00
}
2022-08-15 19:20:10 +12:00
Authorization::setRole(Auth::USER_ROLE_APPS);
2022-07-22 18:00:42 +12:00
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
2022-08-06 00:08:04 +12:00
2022-08-20 19:41:07 +12:00
$accessedAt = $key->getAttribute('accessedAt', '');
2022-08-20 02:52:38 +12:00
if (DateTime::formatTz(DateTime::addSeconds(new \DateTime(), -APP_KEY_ACCCESS)) > $accessedAt) {
$key->setAttribute('accessedAt', DateTime::now());
2022-08-06 00:08:04 +12:00
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
}
2022-08-09 18:13:34 +12:00
2022-08-11 01:45:04 +12:00
$sdkValidator = new WhiteList($servers, true);
$sdk = $request->getHeader('x-sdk-name', 'UNKNOWN');
2022-08-09 18:13:34 +12:00
if ($sdkValidator->isValid($sdk)) {
$sdks = $key->getAttribute('sdks', []);
if (!in_array($sdk, $sdks)) {
2022-08-11 01:49:56 +12:00
array_push($sdks, $sdk);
2022-08-09 18:13:34 +12:00
$key->setAttribute('sdks', $sdks);
2022-08-11 01:49:56 +12:00
2022-08-09 20:52:30 +12:00
/** Update access time as well */
2022-08-20 02:52:38 +12:00
$key->setAttribute('accessedAt', Datetime::now());
2022-08-09 18:13:34 +12:00
$dbForConsole->updateDocument('keys', $key->getId(), $key);
$dbForConsole->deleteCachedDocument('projects', $project->getId());
}
}
2021-12-27 23:35:51 +13:00
}
2022-07-22 18:00:42 +12:00
}
2021-12-27 23:35:51 +13:00
Authorization::setRole($role);
2022-07-22 18:00:42 +12:00
foreach (Auth::getRoles($user) as $authRole) {
Authorization::setRole($authRole);
}
$service = $route->getLabel('sdk.namespace', '');
if (!empty($service)) {
if (
array_key_exists($service, $project->getAttribute('services', []))
&& !$project->getAttribute('services', [])[$service]
&& !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles()))
) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_SERVICE_DISABLED);
2021-12-07 02:14:55 +13:00
}
2022-07-22 18:00:42 +12:00
}
2022-07-22 18:00:42 +12:00
if (!\in_array($scope, $scopes)) {
if ($project->isEmpty()) { // Check if permission is denied because project is missing
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::PROJECT_NOT_FOUND);
2021-12-07 02:14:55 +13:00
}
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, $user->getAttribute('email', 'User') . ' (role: ' . \strtolower($roles[$role]['label']) . ') missing scope (' . $scope . ')');
2021-12-07 02:14:55 +13:00
}
2019-05-09 18:54:39 +12:00
2022-07-22 18:00:42 +12:00
if (false === $user->getAttribute('status')) { // Account is blocked
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::USER_BLOCKED);
2022-07-22 18:00:42 +12:00
}
2022-03-02 05:16:42 +13:00
2022-07-22 18:00:42 +12:00
if ($user->getAttribute('reset')) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::USER_PASSWORD_RESET_REQUIRED);
2022-07-22 18:00:42 +12:00
}
});
2022-03-02 03:19:47 +13:00
2022-07-22 18:00:42 +12:00
App::options()
2023-07-11 20:53:40 +12:00
->inject('utopia')
2023-02-28 01:43:20 +13:00
->inject('swooleRequest')
2022-07-22 18:00:42 +12:00
->inject('request')
->inject('response')
2023-02-28 01:43:20 +13:00
->inject('dbForConsole')
2023-07-11 20:53:40 +12:00
->action(function (App $utopia, SwooleRequest $swooleRequest, Request $request, Response $response, Database $dbForConsole) {
2023-02-28 01:43:20 +13:00
/*
* Appwrite Router
*/
2023-07-28 19:56:07 +12:00
$host = $request->getHostname() ?? '';
2023-02-28 01:43:20 +13:00
$mainDomain = App::getEnv('_APP_DOMAIN', '');
// Only run Router when external domain
if ($host !== $mainDomain) {
2023-07-28 19:56:07 +12:00
if (router($utopia, $dbForConsole, $swooleRequest, $request, $response)) {
2023-02-28 01:43:20 +13:00
return;
}
}
2022-07-22 18:00:42 +12:00
$origin = $request->getOrigin();
$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-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-Fallback-Cookies')
2022-07-22 18:00:42 +12:00
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->noContent();
});
App::error()
->inject('error')
->inject('utopia')
->inject('request')
->inject('response')
->inject('project')
->inject('logger')
->inject('loggerBreadcrumbs')
2022-11-18 01:37:59 +13:00
->action(function (Throwable $error, App $utopia, Request $request, Response $response, Document $project, ?Logger $logger, array $loggerBreadcrumbs) {
2022-07-22 18:00:42 +12:00
$version = App::getEnv('_APP_VERSION', 'UNKNOWN');
2023-02-20 00:04:12 +13:00
$route = $utopia->getRoute();
2022-07-22 18:00:42 +12:00
if ($logger) {
if ($error->getCode() >= 500 || $error->getCode() === 0) {
try {
/** @var Utopia\Database\Document $user */
$user = $utopia->getResource('user');
} catch (\Throwable $th) {
// All good, user is optional information for logger
}
$log = new Utopia\Logger\Log();
if (isset($user) && !$user->isEmpty()) {
$log->setUser(new User($user->getId()));
}
$log->setNamespace("http");
$log->setServer(\gethostname());
$log->setVersion($version);
$log->setType(Log::TYPE_ERROR);
$log->setMessage($error->getMessage());
2023-06-15 23:21:18 +12:00
$log->addTag('database', $project->getAttribute('database', 'console'));
2022-07-22 18:00:42 +12:00
$log->addTag('method', $route->getMethod());
$log->addTag('url', $route->getPath());
$log->addTag('verboseType', get_class($error));
$log->addTag('code', $error->getCode());
$log->addTag('projectId', $project->getId());
$log->addTag('hostname', $request->getHostname());
$log->addTag('locale', (string)$request->getParam('locale', $request->getHeader('x-appwrite-locale', '')));
$log->addExtra('file', $error->getFile());
$log->addExtra('line', $error->getLine());
$log->addExtra('trace', $error->getTraceAsString());
$log->addExtra('detailedTrace', $error->getTrace());
$log->addExtra('roles', Authorization::getRoles());
2022-07-22 18:00:42 +12:00
$action = $route->getLabel("sdk.namespace", "UNKNOWN_NAMESPACE") . '.' . $route->getLabel("sdk.method", "UNKNOWN_METHOD");
$log->setAction($action);
$isProduction = App::getEnv('_APP_ENV', 'development') === 'production';
$log->setEnvironment($isProduction ? Log::ENVIRONMENT_PRODUCTION : Log::ENVIRONMENT_STAGING);
foreach ($loggerBreadcrumbs as $loggerBreadcrumb) {
$log->addBreadcrumb($loggerBreadcrumb);
}
$responseCode = $logger->addLog($log);
Console::info('Log pushed with status code: ' . $responseCode);
}
}
$code = $error->getCode();
$message = $error->getMessage();
$file = $error->getFile();
$line = $error->getLine();
$trace = $error->getTrace();
if (php_sapi_name() === 'cli') {
Console::error('[Error] Timestamp: ' . date('c', time()));
if ($route) {
Console::error('[Error] Method: ' . $route->getMethod());
Console::error('[Error] URL: ' . $route->getPath());
}
Console::error('[Error] Type: ' . get_class($error));
Console::error('[Error] Message: ' . $message);
Console::error('[Error] File: ' . $file);
Console::error('[Error] Line: ' . $line);
}
/** Handle Utopia Errors */
if ($error instanceof Utopia\Exception) {
2022-08-09 02:44:07 +12:00
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
2022-07-22 18:00:42 +12:00
switch ($code) {
case 400:
$error->setType(AppwriteException::GENERAL_ARGUMENT_INVALID);
break;
case 404:
$error->setType(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
break;
}
} elseif ($error instanceof Utopia\Database\Exception\Conflict) {
2023-10-19 15:32:45 +13:00
$error = new AppwriteException(AppwriteException::DOCUMENT_UPDATE_CONFLICT, previous: $error);
$code = $error->getCode();
$message = $error->getMessage();
} elseif ($error instanceof Utopia\Database\Exception\Timeout) {
2023-10-19 17:12:05 +13:00
$error = new AppwriteException(AppwriteException::DATABASE_TIMEOUT, previous: $error);
$code = $error->getCode();
$message = $error->getMessage();
2022-07-22 18:00:42 +12:00
}
/** Wrap all exceptions inside Appwrite\Extend\Exception */
if (!($error instanceof AppwriteException)) {
2022-08-09 02:44:07 +12:00
$error = new AppwriteException(AppwriteException::GENERAL_UNKNOWN, $message, $code, $error);
2022-07-22 18:00:42 +12:00
}
switch ($code) { // Don't show 500 errors!
case 400: // Error allowed publicly
case 401: // Error allowed publicly
case 402: // Error allowed publicly
case 403: // Error allowed publicly
case 404: // Error allowed publicly
2023-10-19 17:06:57 +13:00
case 408: // Error allowed publicly
2022-07-22 18:00:42 +12:00
case 409: // Error allowed publicly
case 412: // Error allowed publicly
case 416: // Error allowed publicly
case 429: // Error allowed publicly
case 501: // Error allowed publicly
case 503: // Error allowed publicly
2022-02-13 22:12:16 +13:00
break;
2022-07-22 18:00:42 +12:00
default:
$code = 500; // All other errors get the generic 500 server error status code
$message = 'Server Error';
2022-02-13 22:12:16 +13:00
}
2019-10-25 06:53:37 +13:00
2022-07-22 18:00:42 +12:00
//$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
$type = $error->getType();
$output = ((App::isDevelopment())) ? [
'message' => $message,
'code' => $code,
'file' => $file,
'line' => $line,
'trace' => $trace,
'version' => $version,
'type' => $type,
] : [
'message' => $message,
'code' => $code,
'version' => $version,
'type' => $type,
];
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
2023-08-07 01:11:30 +12:00
->setStatusCode($code);
2019-10-25 06:53:37 +13:00
2022-07-22 18:00:42 +12:00
$template = ($route) ? $route->getLabel('error', null) : null;
if ($template) {
2022-11-18 01:37:59 +13:00
$layout = new View($template);
2022-07-22 18:00:42 +12:00
2022-11-18 01:37:59 +13:00
$layout
->setParam('title', $project->getAttribute('name') . ' - Error')
2022-07-22 18:00:42 +12:00
->setParam('development', App::isDevelopment())
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $error->getMessage())
2023-08-01 07:55:02 +12:00
->setParam('type', $type)
2022-07-22 18:00:42 +12:00
->setParam('code', $code)
2023-08-07 01:11:30 +12:00
->setParam('trace', $trace);
2022-07-22 18:00:42 +12:00
$response->html($layout->render());
}
2019-10-25 06:53:37 +13:00
2022-07-22 18:00:42 +12:00
$response->dynamic(
new Document($output),
$utopia->isDevelopment() ? Response::MODEL_ERROR_DEV : Response::MODEL_ERROR
);
});
2019-10-25 06:53:37 +13:00
2020-06-29 05:31:21 +12:00
App::get('/robots.txt')
2019-10-25 06:53:37 +13:00
->desc('Robots.txt File')
->label('scope', 'public')
->label('docs', false)
2020-12-27 01:19:46 +13:00
->inject('response')
2022-05-27 02:46:08 +12:00
->action(function (Response $response) {
2022-05-31 23:35:59 +12:00
$template = new View(__DIR__ . '/../views/general/robots.phtml');
2020-06-30 09:43:34 +12:00
$response->text($template->render(false));
2020-12-27 01:19:46 +13:00
});
2019-10-25 06:53:37 +13:00
2020-06-29 05:31:21 +12:00
App::get('/humans.txt')
2019-10-25 06:53:37 +13:00
->desc('Humans.txt File')
->label('scope', 'public')
->label('docs', false)
2020-12-27 01:19:46 +13:00
->inject('response')
2022-05-27 02:46:08 +12:00
->action(function (Response $response) {
2022-05-31 23:35:59 +12:00
$template = new View(__DIR__ . '/../views/general/humans.phtml');
2020-06-30 09:43:34 +12:00
$response->text($template->render(false));
2020-12-27 01:19:46 +13:00
});
2019-10-25 06:53:37 +13:00
2023-06-02 23:18:34 +12:00
App::get('/.well-known/acme-challenge/*')
2020-02-19 11:13:18 +13:00
->desc('SSL Verification')
->label('scope', 'public')
->label('docs', false)
2020-12-27 01:19:46 +13:00
->inject('request')
->inject('response')
2022-05-27 02:46:08 +12:00
->action(function (Request $request, Response $response) {
$uriChunks = \explode('/', $request->getURI());
$token = $uriChunks[\count($uriChunks) - 1];
2023-03-02 01:00:36 +13:00
$validator = new Text(100, allowList: [
...Text::NUMBERS,
...Text::ALPHABET_LOWER,
...Text::ALPHABET_UPPER,
'-',
'_'
]);
2022-02-01 04:04:30 +13:00
if (!$validator->isValid($token) || \count($uriChunks) !== 4) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_ARGUMENT_INVALID, 'Invalid challenge token.');
2022-02-01 04:04:30 +13:00
}
2020-06-30 09:43:34 +12:00
$base = \realpath(APP_STORAGE_CERTIFICATES);
2022-05-24 02:54:50 +12:00
$absolute = \realpath($base . '/.well-known/acme-challenge/' . $token);
2020-02-23 21:55:57 +13:00
2020-10-28 08:46:15 +13:00
if (!$base) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Storage error');
2020-06-30 09:43:34 +12:00
}
2020-02-19 11:13:18 +13:00
2020-10-28 08:46:15 +13:00
if (!$absolute) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
2020-06-30 09:43:34 +12:00
}
2020-02-23 21:55:57 +13:00
2020-10-28 08:46:15 +13:00
if (!\substr($absolute, 0, \strlen($base)) === $base) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_UNAUTHORIZED_SCOPE, 'Invalid path');
2020-06-30 09:43:34 +12:00
}
2020-02-24 06:45:51 +13:00
2020-10-28 08:46:15 +13:00
if (!\file_exists($absolute)) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND, 'Unknown path');
2020-06-30 09:43:34 +12:00
}
2020-02-23 21:55:57 +13:00
2020-06-30 09:43:34 +12:00
$content = @\file_get_contents($absolute);
2020-02-19 11:13:18 +13:00
2020-10-28 08:46:15 +13:00
if (!$content) {
2022-08-09 02:44:07 +12:00
throw new AppwriteException(AppwriteException::GENERAL_SERVER_ERROR, 'Failed to get contents');
2020-02-19 11:13:18 +13:00
}
2020-06-30 09:43:34 +12:00
$response->text($content);
2020-12-27 01:19:46 +13:00
});
2020-02-19 11:13:18 +13:00
2020-07-29 19:29:34 +12:00
include_once __DIR__ . '/shared/api.php';
2022-11-24 20:53:52 +13:00
include_once __DIR__ . '/shared/api/auth.php';
2020-06-26 07:53:36 +12:00
2023-02-16 03:50:18 +13:00
App::wildcard()
2023-08-07 01:11:30 +12:00
->groups(['api'])
2023-09-07 16:57:23 +12:00
->label('scope', 'global')
2023-02-21 23:31:55 +13:00
->action(function () {
2023-02-16 03:50:18 +13:00
throw new AppwriteException(AppwriteException::GENERAL_ROUTE_NOT_FOUND);
});
2020-10-28 08:46:15 +13:00
foreach (Config::getParam('services', []) as $service) {
2020-06-26 18:14:54 +12:00
include_once $service['controller'];
}