1
0
Fork 0
mirror of synced 2024-06-02 19:04:49 +12:00
appwrite/app/app.php

430 lines
15 KiB
PHP
Raw Normal View History

2019-05-09 18:54:39 +12:00
<?php
// Init
2019-10-25 06:53:37 +13:00
require_once __DIR__.'/init.php';
2020-03-29 01:42:16 +13:00
global $utopia, $request, $response, $register, $consoleDB, $project, $service;
2019-05-09 18:54:39 +12:00
use Utopia\App;
use Utopia\Request;
2019-10-25 06:53:37 +13:00
use Utopia\View;
use Utopia\Exception;
2020-03-29 01:42:16 +13:00
use Utopia\Config\Config;
use Utopia\Domains\Domain;
use Appwrite\Auth\Auth;
use Appwrite\Database\Database;
use Appwrite\Database\Document;
use Appwrite\Database\Validator\Authorization;
use Appwrite\Event\Event;
2020-04-14 17:44:15 +12:00
use Appwrite\Network\Validators\Origin;
2019-05-09 18:54:39 +12:00
2019-10-01 17:57:41 +13:00
/*
2019-10-25 06:53:37 +13:00
* Configuration files
2019-05-09 18:54:39 +12:00
*/
2019-10-25 06:53:37 +13:00
$roles = include __DIR__.'/config/roles.php'; // User roles and scopes
$services = include __DIR__.'/config/services.php'; // List of services
2019-10-25 06:53:37 +13:00
$webhook = new Event('v1-webhooks', 'WebhooksV1');
$audit = new Event('v1-audits', 'AuditsV1');
$usage = new Event('v1-usage', 'UsageV1');
$deletes = new Event('v1-deletes', 'DeletesV1');
2019-10-01 17:57:41 +13:00
2019-10-25 06:53:37 +13:00
/**
* Get All verified client URLs for both console and current projects
* + Filter for duplicated entries
2019-10-25 06:53:11 +13:00
*/
2019-10-25 06:53:37 +13:00
$clientsConsole = array_map(function ($node) {
return $node['hostname'];
}, array_filter($console->getAttribute('platforms', []), function ($node) {
if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) {
return true;
}
2019-05-09 18:54:39 +12:00
return false;
}));
2019-10-25 06:53:37 +13:00
$clients = array_unique(array_merge($clientsConsole, array_map(function ($node) {
return $node['hostname'];
}, array_filter($project->getAttribute('platforms', []), function ($node) {
if (isset($node['type']) && $node['type'] === 'web' && isset($node['hostname']) && !empty($node['hostname'])) {
return true;
}
2019-05-09 18:54:39 +12:00
return false;
}))));
2019-10-25 06:53:37 +13:00
2020-04-15 07:53:41 +12:00
$utopia->init(function () use ($utopia, $request, $response, &$user, $project, $console, $roles, $webhook, $audit, $usage, $clients) {
2019-10-25 06:53:37 +13:00
$route = $utopia->match($request);
2020-05-01 22:07:53 +12:00
if(!empty($route->getLabel('sdk.platform', [])) && empty($project->getId()) && ($route->getLabel('scope', '') !== 'public')) {
2020-04-08 23:21:41 +12:00
throw new Exception('Missing or unknown project ID', 400);
2020-04-07 18:10:58 +12:00
}
2019-10-25 06:53:37 +13:00
$referrer = $request->getServer('HTTP_REFERER', '');
$origin = parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_HOST);
2020-03-17 08:07:43 +13:00
$protocol = parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_SCHEME);
$port = parse_url($request->getServer('HTTP_ORIGIN', $referrer), PHP_URL_PORT);
2019-10-25 06:53:37 +13:00
$refDomain = $protocol.'://'.((in_array($origin, $clients))
2020-03-17 08:07:43 +13:00
? $origin : 'localhost') . (!empty($port) ? ':'.$port : '');
2019-10-25 06:53:37 +13:00
$selfDomain = new Domain(Config::getParam('hostname'));
$endDomain = new Domain($origin);
2020-04-07 18:10:58 +12:00
Config::setParam('domainVerification',
($selfDomain->getRegisterable() === $endDomain->getRegisterable()) &&
$endDomain->getRegisterable() !== '');
2020-04-07 18:10:58 +12:00
2019-10-25 06:53:37 +13:00
/*
* Security Headers
*
* As recommended at:
* @see https://www.owasp.org/index.php/List_of_useful_HTTP_headers
*/
$response
->addHeader('Server', 'Appwrite')
->addHeader('X-XSS-Protection', '1; mode=block; report=/v1/xss?url='.urlencode($request->getServer('REQUEST_URI')))
//->addHeader('X-Frame-Options', ($refDomain == 'http://localhost') ? 'SAMEORIGIN' : 'ALLOW-FROM ' . $refDomain)
->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-SDK-Version, Cache-Control, Expires, Pragma')
2020-02-27 22:17:09 +13:00
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
2019-10-25 06:53:37 +13:00
->addHeader('Access-Control-Allow-Origin', $refDomain)
->addHeader('Access-Control-Allow-Credentials', 'true')
;
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
/*
* Validate Client Domain - Check to avoid CSRF attack
* Adding Appwrite API domains to allow XDOMAIN communication
2020-01-29 23:49:18 +13:00
* Skip this check for non-web platforms which are not requiredto send an origin header
2019-10-25 06:53:37 +13:00
*/
2020-04-14 17:44:15 +12:00
$origin = $request->getServer('HTTP_ORIGIN', $request->getServer('HTTP_REFERER', ''));
2020-04-15 07:53:41 +12:00
$originValidator = new Origin(array_merge($project->getAttribute('platforms', []), $console->getAttribute('platforms', [])));
2020-04-14 17:44:15 +12:00
if(!$originValidator->isValid($origin)
2019-10-25 06:53:37 +13:00
&& in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE])
&& $route->getLabel('origin', false) !== '*'
2020-04-14 17:44:15 +12:00
&& empty($request->getHeader('X-Appwrite-Key', ''))) {
throw new Exception($originValidator->getDescription(), 403);
2019-10-25 06:53:37 +13:00
}
2020-04-14 17:44:15 +12:00
2019-10-25 06:53:37 +13:00
/*
* ACL Check
*/
$role = ($user->isEmpty()) ? Auth::USER_ROLE_GUEST : Auth::USER_ROLE_MEMBER;
// Add user roles
$membership = $user->search('teamId', $project->getAttribute('teamId', null), $user->getAttribute('memberships', []));
if ($membership) {
foreach ($membership->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;
}
}
}
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
$scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route
$scopes = $roles[$role]['scopes']; // Allowed scopes for user role
2020-01-12 10:53:57 +13:00
2019-10-25 06:53:37 +13:00
// Check if given key match project API keys
$key = $project->search('secret', $request->getHeader('X-Appwrite-Key', ''), $project->getAttribute('keys', []));
2020-01-12 10:53:57 +13:00
2019-10-25 06:53:37 +13:00
/*
* 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 (null !== $key && $user->isEmpty()) {
$user = new Document([
2020-02-17 20:16:11 +13:00
'$id' => 0,
2019-10-25 06:53:37 +13:00
'status' => Auth::USER_STATUS_ACTIVATED,
2020-03-29 01:42:16 +13:00
'email' => 'app.'.$project->getId().'@service.'.Config::getParam('domain'),
2019-10-25 06:53:37 +13:00
'password' => '',
'name' => $project->getAttribute('name', 'Untitled'),
]);
$role = Auth::USER_ROLE_APP;
2020-01-13 04:20:51 +13:00
$scopes = array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', []));
2019-10-25 06:53:37 +13:00
2020-02-11 19:17:40 +13:00
Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys.
2019-10-25 06:53:37 +13:00
}
2019-05-09 18:54:39 +12:00
2020-02-17 20:16:11 +13:00
Authorization::setRole('user:'.$user->getId());
2019-10-25 06:53:37 +13:00
Authorization::setRole('role:'.$role);
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
array_map(function ($node) {
if (isset($node['teamId']) && isset($node['roles'])) {
Authorization::setRole('team:'.$node['teamId']);
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
foreach ($node['roles'] as $nodeRole) { // Set all team roles
Authorization::setRole('team:'.$node['teamId'].'/'.$nodeRole);
}
}
}, $user->getAttribute('memberships', []));
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
// TDOO Check if user is god
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
if (!in_array($scope, $scopes)) {
2020-02-17 20:16:11 +13:00
if (empty($project->getId()) || Database::SYSTEM_COLLECTION_PROJECTS !== $project->getCollection()) { // Check if permission is denied because project is missing
throw new Exception('Project not found', 404);
}
2019-10-25 06:53:37 +13:00
throw new Exception($user->getAttribute('email', 'Guest').' (role: '.strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401);
}
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
if (Auth::USER_STATUS_BLOCKED == $user->getAttribute('status')) { // Account has not been activated
throw new Exception('Invalid credentials. User is blocked', 401); // User is in status blocked
}
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
if ($user->getAttribute('reset')) {
throw new Exception('Password reset is required', 412);
}
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
/*
* Background Jobs
*/
$webhook
2020-02-17 20:16:11 +13:00
->setParam('projectId', $project->getId())
2019-10-25 06:53:37 +13:00
->setParam('event', $route->getLabel('webhook', ''))
->setParam('payload', [])
;
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
$audit
2020-02-17 20:16:11 +13:00
->setParam('projectId', $project->getId())
->setParam('userId', $user->getId())
2019-10-25 06:53:37 +13:00
->setParam('event', '')
->setParam('resource', '')
->setParam('userAgent', $request->getServer('HTTP_USER_AGENT', ''))
->setParam('ip', $request->getIP())
->setParam('data', [])
;
2019-10-25 06:53:37 +13:00
$usage
2020-02-17 20:16:11 +13:00
->setParam('projectId', $project->getId())
2019-10-25 06:53:37 +13:00
->setParam('url', $request->getServer('HTTP_HOST', '').$request->getServer('REQUEST_URI', ''))
->setParam('method', $request->getServer('REQUEST_METHOD', 'UNKNOWN'))
->setParam('request', 0)
->setParam('response', 0)
->setParam('storage', 0)
;
});
2019-05-09 18:54:39 +12:00
$utopia->shutdown(function () use ($response, $request, $webhook, $audit, $usage, $deletes, $mode, $project, $utopia) {
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
/*
* Trigger events for background workers
2019-10-25 06:53:37 +13:00
*/
if (!empty($webhook->getParam('event'))) {
$webhook->trigger();
}
2020-01-12 02:58:02 +13:00
2019-10-25 06:53:37 +13:00
if (!empty($audit->getParam('event'))) {
$audit->trigger();
}
if (!empty($deletes->getParam('document'))) {
$deletes->trigger();
}
$route = $utopia->match($request);
2019-05-09 18:54:39 +12:00
2020-02-17 20:16:11 +13:00
if($project->getId()
&& $mode !== APP_MODE_ADMIN
&& !empty($route->getLabel('sdk.namespace', null))) { // Don't calculate console usage and admin mode
$usage
->setParam('request', $request->getSize())
->setParam('response', $response->getSize())
->trigger()
;
}
2019-10-25 06:53:37 +13:00
});
2019-05-09 18:54:39 +12:00
2020-03-17 08:07:43 +13:00
$utopia->options(function () use ($request, $response) {
2019-10-25 06:53:37 +13:00
$origin = $request->getServer('HTTP_ORIGIN');
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
$response
->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-SDK-Version, Cache-Control, Expires, Pragma, X-Fallback-Cookies')
2020-02-27 22:17:09 +13:00
->addHeader('Access-Control-Expose-Headers', 'X-Fallback-Cookies')
2019-10-25 06:53:37 +13:00
->addHeader('Access-Control-Allow-Origin', $origin)
->addHeader('Access-Control-Allow-Credentials', 'true')
->send();
});
2019-05-09 18:54:39 +12:00
2020-03-29 01:42:16 +13:00
$utopia->error(function ($error /* @var $error Exception */) use ($request, $response, $utopia, $project) {
$env = Config::getParam('env');
$version = Config::getParam('version');
2019-10-25 06:53:37 +13:00
switch ($error->getCode()) {
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
2020-01-04 10:01:09 +13:00
case 409: // Error allowed publicly
2019-10-25 06:53:37 +13:00
case 412: // Error allowed publicly
case 429: // Error allowed publicly
$code = $error->getCode();
$message = $error->getMessage();
break;
default:
$code = 500; // All other errors get the generic 500 server error status code
$message = 'Server Error';
}
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
$_SERVER = []; // Reset before reporting to error log to avoid keys being compromised
$output = ((App::ENV_TYPE_DEVELOPMENT == $env)) ? [
'message' => $error->getMessage(),
'code' => $error->getCode(),
'file' => $error->getFile(),
'line' => $error->getLine(),
'trace' => $error->getTrace(),
'version' => $version,
] : [
'message' => $message,
'code' => $code,
'version' => $version,
];
$response
->addHeader('Cache-Control', 'no-cache, no-store, must-revalidate')
->addHeader('Expires', '0')
->addHeader('Pragma', 'no-cache')
->setStatusCode($code)
2019-10-25 06:53:11 +13:00
;
2019-05-09 18:54:39 +12:00
2019-10-25 06:53:37 +13:00
$route = $utopia->match($request);
$template = ($route) ? $route->getLabel('error', null) : null;
if ($template) {
$layout = new View(__DIR__.'/views/layouts/default.phtml');
$comp = new View($template);
$comp
->setParam('projectName', $project->getAttribute('name'))
->setParam('projectURL', $project->getAttribute('url'))
->setParam('message', $error->getMessage())
->setParam('code', $code)
;
$layout
->setParam('title', $project->getAttribute('name').' - Error')
->setParam('description', 'No Description')
->setParam('body', $comp)
->setParam('version', $version)
->setParam('litespeed', false)
;
$response->send($layout->render());
2019-10-25 06:53:11 +13:00
}
2019-10-25 06:53:37 +13:00
$response
->json($output)
;
});
$utopia->get('/manifest.json')
->desc('Progressive app manifest file')
->label('scope', 'public')
->label('docs', false)
->action(
function () use ($response) {
$response->json([
'name' => APP_NAME,
'short_name' => APP_NAME,
'start_url' => '.',
'url' => 'https://appwrite.io/',
'display' => 'standalone',
'background_color' => '#fff',
'theme_color' => '#f02e65',
'description' => 'End to end backend server for frontend and mobile apps. 👩‍💻👨‍💻',
'icons' => [
[
'src' => 'images/favicon.png',
'sizes' => '256x256',
'type' => 'image/png',
],
],
]);
}
);
$utopia->get('/robots.txt')
->desc('Robots.txt File')
->label('scope', 'public')
->label('docs', false)
->action(
function () use ($response) {
$template = new View(__DIR__.'/views/general/robots.phtml');
$response->text($template->render(false));
2019-10-25 06:53:37 +13:00
}
);
$utopia->get('/humans.txt')
->desc('Humans.txt File')
->label('scope', 'public')
->label('docs', false)
->action(
function () use ($response) {
$template = new View(__DIR__.'/views/general/humans.phtml');
$response->text($template->render(false));
2019-10-25 06:53:37 +13:00
}
);
2020-02-19 11:13:18 +13:00
$utopia->get('/.well-known/acme-challenge')
->desc('SSL Verification')
->label('scope', 'public')
->label('docs', false)
->action(
function () use ($request, $response) {
2020-02-23 21:55:57 +13:00
$base = realpath(APP_STORAGE_CERTIFICATES);
2020-02-19 11:13:18 +13:00
$path = str_replace('/.well-known/acme-challenge/', '', $request->getParam('q'));
2020-02-23 21:55:57 +13:00
$absolute = realpath($base.'/.well-known/acme-challenge/'.$path);
if(!$base) {
throw new Exception('Storage error', 500);
}
2020-02-19 11:13:18 +13:00
if(!$absolute) {
2020-02-23 21:55:57 +13:00
throw new Exception('Unknown path', 404);
2020-02-19 11:13:18 +13:00
}
if(!substr($absolute, 0, strlen($base)) === $base) {
2020-02-23 21:55:57 +13:00
throw new Exception('Invalid path', 401);
}
2020-02-24 06:45:51 +13:00
if(!file_exists($absolute)) {
throw new Exception('Unknown path', 404);
}
2020-02-23 21:55:57 +13:00
$content = @file_get_contents($absolute);
if(!$content) {
throw new Exception('Failed to get contents', 500);
2020-02-19 11:13:18 +13:00
}
2020-02-23 21:55:57 +13:00
$response->text($content);
2020-02-19 11:13:18 +13:00
}
);
2019-10-25 06:53:37 +13:00
$name = APP_NAME;
if (array_key_exists($service, $services)) { /** @noinspection PhpIncludeInspection */
include_once $services[$service]['controller'];
$name = APP_NAME.' '.ucfirst($services[$service]['name']);
} else {
/** @noinspection PhpIncludeInspection */
include_once $services['/']['controller'];
2019-05-09 18:54:39 +12:00
}
2020-03-25 05:24:56 +13:00
$utopia->run($request, $response);