getAttribute('platforms', []), function ($node) { if (isset($node['type']) && $node['type'] === 'web' && isset($node['url']) && !empty($node['url'])) { return true; } return false; })); $clients = array_merge($clientsConsole, array_map(function ($node) { return $node['url']; }, array_filter($project->getAttribute('platforms', []), function ($node) { if (isset($node['type']) && $node['type'] === 'web' && isset($node['url']) && !empty($node['url'])) { return true; } return false; }))); $utopia->init(function () use ($utopia, $request, $response, $register, &$user, $project, $roles, $webhook, $audit, $usage, $domain, $clients) { $route = $utopia->match($request); $referrer = $request->getServer('HTTP_REFERER', ''); $origin = $request->getServer('HTTP_ORIGIN', parse_url($referrer, PHP_URL_SCHEME).'://'.parse_url($referrer, PHP_URL_HOST)); $refDomain = (in_array($origin, $clients)) ? $origin : 'http://localhost'; /* * 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, X-Requested-With, Content-Type, Access-Control-Allow-Origin, Access-Control-Request-Headers, Accept, X-Appwrite-Project, X-Appwrite-Key, X-Appwrite-Locale, X-SDK-Version') ->addHeader('Access-Control-Allow-Origin', $refDomain) ->addHeader('Access-Control-Allow-Credentials', 'true') ; /* * Validate Client Domain - Check to avoid CSRF attack * Adding appwrite api domains to allow XDOMAIN communication */ $hostValidator = new Host($clients); if (!$hostValidator->isValid($request->getServer('HTTP_ORIGIN', $request->getServer('HTTP_REFERER', ''))) && in_array($request->getMethod(), [Request::METHOD_POST, Request::METHOD_PUT, Request::METHOD_PATCH, Request::METHOD_DELETE]) && empty($request->getHeader('X-Appwrite-Key', ''))) { throw new Exception('Access from this client host is forbidden. '.$hostValidator->getDescription(), 403); } /* * 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; } } } $scope = $route->getLabel('scope', 'none'); // Allowed scope for chosen route $scopes = $roles[$role]['scopes']; // Allowed scopes for user role // Check if given key match project API keys $key = $project->search('secret', $request->getHeader('X-Appwrite-Key', ''), $project->getAttribute('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 (null !== $key && $user->isEmpty()) { $user = new Document([ '$uid' => 0, 'status' => Auth::USER_STATUS_ACTIVATED, 'email' => 'app.'.$project->getUid().'@service.'.$domain, 'password' => '', 'name' => $project->getAttribute('name', 'Untitled'), ]); $role = Auth::USER_ROLE_APP; $scopes = array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); Authorization::disable(); // Cancel security segmentation for API keys. } Authorization::setRole('user:'.$user->getUid()); Authorization::setRole('role:'.$role); array_map(function ($node) { if (isset($node['teamId']) && isset($node['roles'])) { Authorization::setRole('team:'.$node['teamId']); foreach ($node['roles'] as $nodeRole) { // Set all team roles Authorization::setRole('team:'.$node['teamId'].'/'.$nodeRole); } } }, $user->getAttribute('memberships', [])); if (!in_array($scope, $scopes)) { throw new Exception($user->getAttribute('email', 'Guest').' (role: '.strtolower($roles[$role]['label']).') missing scope ('.$scope.')', 401); } 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 } if ($user->getAttribute('reset')) { throw new Exception('Password reset is required', 412); } /* * Background Jobs */ $webhook ->setParam('projectId', $project->getUid()) ->setParam('event', $route->getLabel('webhook', '')) ->setParam('payload', []) ; $audit ->setParam('projectId', $project->getUid()) ->setParam('userId', $user->getUid()) ->setParam('event', '') ->setParam('resource', '') ->setParam('userAgent', $request->getServer('HTTP_USER_AGENT', '')) ->setParam('ip', $request->getIP()) ->setParam('data', []) ; $usage ->setParam('projectId', $project->getUid()) ->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) ; /* * Abuse Check */ $timeLimit = new TimeLimit($route->getLabel('abuse-key', 'url:{url},ip:{ip}'), $route->getLabel('abuse-limit', 0), $route->getLabel('abuse-time', 3600), function () use ($register) { return $register->get('db'); }); $timeLimit->setNamespace('app_'.$project->getUid()); $timeLimit ->setParam('{userId}', $user->getUid()) ->setParam('{userAgent}', $request->getServer('HTTP_USER_AGENT', '')) ->setParam('{ip}', $request->getIP()) ->setParam('{url}', $request->getServer('HTTP_HOST', '').$route->getURL()) ; //TODO make sure we get array here foreach ($request->getParams() as $key => $value) { // Set request params as potential abuse keys $timeLimit->setParam('{param-'.$key.'}', (is_array($value)) ? json_encode($value) : $value); } $abuse = new Abuse($timeLimit); if ($timeLimit->limit()) { $response ->addHeader('X-RateLimit-Limit', $timeLimit->limit()) ->addHeader('X-RateLimit-Remaining', $timeLimit->remaining()) ->addHeader('X-RateLimit-Reset', $timeLimit->time() + $route->getLabel('abuse-time', 3600)) ; } if ($abuse->check() && $request->getServer('_APP_OPTIONS_ABUSE', 'enabled') !== 'disabled') { throw new Exception('Too many requests', 429); } }); $utopia->shutdown(function () use ($response, $request, $webhook, $audit, $usage) { /* * Trigger Events for background jobs */ if (!empty($webhook->getParam('event'))) { $webhook->trigger(); } if (!empty($audit->getParam('event'))) { $audit->trigger(); } $usage ->setParam('request', $request->getSize()) ->setParam('response', $response->getSize()) ->trigger() ; }); $utopia->options(function () use ($request, $response, $domain, $project) { $origin = $request->getServer('HTTP_ORIGIN'); $response ->addHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE') ->addHeader('Access-Control-Allow-Headers', 'Origin, 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-SDK-Version') ->addHeader('Access-Control-Allow-Origin', $origin) ->addHeader('Access-Control-Allow-Credentials', 'true') ->send(); }); $utopia->error(function ($error /* @var $error Exception */) use ($request, $response, $utopia, $project, $env, $version, $sentry, $user) { 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 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'; } $_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) ; $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()); } $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) { $response->text('# robotstxt.org/ User-agent: * '); } ); $utopia->get('/humans.txt') ->desc('Humans.txt File') ->label('scope', 'public') ->label('docs', false) ->action( function () use ($response) { $response->text('# humanstxt.org/ # The humans responsible & technology colophon # TEAM -- -- # THANKS '); } ); $utopia->get('/v1/info') // This is only visible to gods ->label('scope', 'god') ->label('docs', false) ->action( function () use ($response, $user, $project, $version, $env) { //TODO CONSIDER BLOCKING THIS ACTION TO ROLE GOD $response->json([ 'name' => 'API', 'version' => $version, 'environment' => $env, 'time' => date('Y-m-d H:i:s', time()), 'user' => [ 'id' => $user->getUid(), 'name' => $user->getAttribute('name', ''), ], 'project' => [ 'id' => $project->getUid(), 'name' => $project->getAttribute('name', ''), ], ]); } ); $utopia->get('/v1/xss') ->desc('Log XSS errors reported by browsers using X-XSS-Protection header') ->label('scope', 'public') ->label('docs', false) ->action( function () { throw new Exception('XSS detected and reported by a browser client', 500); } ); $utopia->get('/v1/proxy') ->label('scope', 'public') ->label('docs', false) ->action( function () use ($response, $console, $clients) { $view = new View(__DIR__.'/views/proxy.phtml'); $view ->setParam('routes', '') ->setParam('clients', array_merge($clients, $console->getAttribute('clients', []))) ; $response ->setContentType(Response::CONTENT_TYPE_HTML) ->removeHeader('X-Frame-Options') ->send($view->render()); } ); $utopia->get('/v1/open-api-2.json') ->label('scope', 'public') ->label('docs', false) ->param('platform', 'client', function () { return new WhiteList(['client', 'server']); }, 'Choose target platform.', true) ->param('extensions', 0, function () { return new Range(0, 1); }, 'Show extra data.', true) ->action( function ($platform, $extensions) use ($response, $request, $utopia, $domain, $version, $services) { function fromCamelCase($input) { preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); $ret = $matches[0]; foreach ($ret as &$match) { $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); } return implode('_', $ret); } function fromCamelCaseToDash($input) { return str_replace([' ', '_'], '-', strtolower(preg_replace('/([a-zA-Z])(?=[A-Z])/', '$1-', $input))); } foreach ($services as $service) { /* @noinspection PhpIncludeInspection */ if (!$service['sdk']) { continue; } /** @noinspection PhpIncludeInspection */ include_once $service['controller']; } $security = [ 'client' => ['Project' => []], 'server' => ['Project' => [], 'Key' => []], ]; /* * Specifications (v3.0.0): * https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md */ $output = [ 'swagger' => '2.0', 'info' => [ 'version' => $version, 'title' => APP_NAME, 'description' => 'Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https://appwrite.io/docs](https://appwrite.io/docs)', 'termsOfService' => 'https://appwrite.io/policy/terms', 'contact' => [ 'name' => 'Appwrite Team', 'url' => 'https://appwrite.io/support', 'email' => APP_EMAIL_TEAM, ], 'license' => [ 'name' => 'BSD-3-Clause', 'url' => 'https://raw.githubusercontent.com/appwrite/appwrite/master/LICENSE', ], ], 'host' => $domain, 'basePath' => '/v1', 'schemes' => ['https'], 'consumes' => ['application/json', 'multipart/form-data'], 'produces' => ['application/json'], 'securityDefinitions' => [ 'Project' => [ 'type' => 'apiKey', 'name' => 'X-Appwrite-Project', 'description' => 'Your Appwrite project ID. You can find your project ID in your Appwrite console project settings.', 'in' => 'header', ], 'Key' => [ 'type' => 'apiKey', 'name' => 'X-Appwrite-Key', 'description' => 'Your Appwrite project secret key. You can can create a new API key from your Appwrite console API keys dashboard.', 'in' => 'header', ], 'Locale' => [ 'type' => 'apiKey', 'name' => 'X-Appwrite-Locale', 'description' => '', 'in' => 'header', ], 'Mode' => [ 'type' => 'apiKey', 'name' => 'X-Appwrite-Mode', 'description' => '', 'in' => 'header', ], ], 'paths' => [], 'definitions' => [ 'Pet' => [ 'required' => ['id', 'name'], 'properties' => [ 'id' => [ 'type' => 'integer', 'format' => 'int64', ], 'name' => [ 'type' => 'string', ], 'tag' => [ 'type' => 'string', ], ], ], 'Pets' => array( 'type' => 'array', 'items' => array( '$ref' => '#/definitions/Pet', ), ), 'Error' => array( 'required' => array( 0 => 'code', 1 => 'message', ), 'properties' => array( 'code' => array( 'type' => 'integer', 'format' => 'int32', ), 'message' => array( 'type' => 'string', ), ), ), ], 'externalDocs' => [ 'description' => 'Full API docs, specs and tutorials', 'url' => $request->getServer('REQUEST_SCHEME', 'https').'://'.$domain.'/docs', ], ]; foreach ($utopia->getRoutes() as $key => $method) { foreach ($method as $route) { /* @var $route \Utopia\Route */ if (!$route->getLabel('docs', true)) { continue; } if (empty($route->getLabel('sdk.namespace', null))) { continue; } $url = str_replace('/v1', '', $route->getURL()); $scope = $route->getLabel('scope', ''); $hide = $route->getLabel('sdk.hide', false); $consumes = []; if ($hide) { continue; } $temp = [ 'summary' => $route->getDesc(), 'operationId' => $route->getLabel('sdk.method', uniqid()), 'consumes' => [], 'tags' => [$route->getLabel('sdk.namespace', 'default')], 'description' => $route->getLabel('sdk.description', ''), 'responses' => [ 200 => [ 'description' => 'An paged array of pets', 'schema' => [ '$ref' => '#/definitions/Pet', ], ], ], ]; if ($extensions) { $temp['extensions'] = [ 'weight' => $route->getOrder(), 'cookies' => $route->getLabel('sdk.cookies', false), 'location' => $route->getLabel('sdk.location', false), 'demo' => 'docs/examples/'.fromCamelCaseToDash($route->getLabel('sdk.namespace', 'default')).'/'.fromCamelCaseToDash($temp['operationId']).'.md', ]; } if ((!empty($scope) && 'public' != $scope)) { $temp['security'][] = $route->getLabel('sdk.security', $security[$platform]); } $requestBody = [ 'content' => [ 'application/x-www-form-urlencoded' => [ 'schema' => [ 'type' => 'object', 'properties' => [], ], 'required' => [], ], ], ]; foreach ($route->getParams() as $name => $param) { $validator = (is_callable($param['validator'])) ? $param['validator']() : $param['validator']; /* @var $validator \Utopia\Validator */ $node = [ 'name' => $name, 'description' => $param['description'], 'required' => !$param['optional'], ]; switch ((!empty($validator)) ? get_class($validator) : '') { case 'Utopia\Validator\Text': $node['type'] = 'string'; $node['x-example'] = '['.strtoupper(fromCamelCase($node['name'])).']'; break; case 'Database\Validator\UID': $node['type'] = 'string'; $node['x-example'] = '['.strtoupper(fromCamelCase($node['name'])).']'; break; case 'Utopia\Validator\Email': $node['type'] = 'string'; $node['format'] = 'email'; $node['x-example'] = 'email@example.com'; break; case 'Utopia\Validator\URL': $node['type'] = 'string'; $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; break; case 'Utopia\Validator\JSON': case 'Utopia\Validator\Mock': $node['type'] = 'object'; $node['type'] = 'string'; $node['x-example'] = '{}'; //$node['format'] = 'json'; break; case 'Storage\Validators\File': $consumes[] = 'multipart/form-data'; $node['type'] = 'file'; break; case 'Utopia\Validator\ArrayList': $node['type'] = 'array'; $node['collectionFormat'] = 'multi'; $node['items'] = [ 'type' => 'string', ]; break; case 'Auth\Validator\Password': $node['type'] = 'string'; $node['format'] = 'format'; $node['x-example'] = 'password'; break; case 'Utopia\Validator\Range': /* @var $validator \Utopia\Validator\Range */ $node['type'] = 'integer'; $node['format'] = 'int32'; $node['x-example'] = rand($validator->getMin(), $validator->getMax()); break; case 'Utopia\Validator\Numeric': $node['type'] = 'integer'; $node['format'] = 'int32'; break; case 'Utopia\Validator\Length': $node['type'] = 'string'; break; case 'Utopia\Validator\Host': $node['type'] = 'string'; $node['format'] = 'url'; $node['x-example'] = 'https://example.com'; break; case 'Utopia\Validator\WhiteList': /* @var $validator \Utopia\Validator\WhiteList */ $node['type'] = 'string'; $node['x-example'] = $validator->getList()[0]; break; default: $node['type'] = 'string'; break; } if ($param['optional'] && !is_null($param['default'])) { // Param has default value $node['default'] = $param['default']; } if (false !== strpos($url, ':'.$name)) { // Param is in URL path $node['in'] = 'path'; $temp['parameters'][] = $node; } elseif ($key == 'GET') { // Param is in query $node['in'] = 'query'; $temp['parameters'][] = $node; } else { // Param is in payload $node['in'] = 'formData'; $temp['parameters'][] = $node; $requestBody['content']['application/x-www-form-urlencoded']['schema']['properties'][] = $node; if (!$param['optional']) { $requestBody['content']['application/x-www-form-urlencoded']['required'][] = $name; } } $url = str_replace(':'.$name, '{'.$name.'}', $url); } $temp['consumes'] = $consumes; $output['paths'][$url][strtolower($route->getMethod())] = $temp; } } /*foreach ($consoleDB->getMocks() as $mock) { var_dump($mock['name']); }*/ ksort($output['paths']); $response->json($output); } ); $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']; } if (extension_loaded('newrelic')) { $route = $utopia->match($request); $url = (!empty($route)) ? $route->getURL() : '/error'; newrelic_set_appname($name); newrelic_name_transaction($request->getServer('REQUEST_METHOD', 'UNKNOWN').': '.$url); } $utopia->run($request, $response);