/* jshint strict: false, unused: false, esnext: true */ /* global JSON_CURSOR */ // // @brief JavaScript actions module // // @file // // DISCLAIMER // // Copyright 2014-2016 ArangoDB GmbH, Cologne, Germany // Copyright 2012-2014 triagens GmbH, Cologne, Germany // // Licensed under the Apache License, Version 2.0 (the "License") // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // Copyright holder is ArangoDB GmbH, Cologne, Germany // // @author Dr. Frank Celler // @author Alan Plum // @author Copyright 2014-2016, ArangoDB GmbH, Cologne, Germany // @author Copyright 2012-2014, triAGENS GmbH, Cologne, Germany // module.isSystem = true; var internal = require('internal'); var Module = require('module'); var fs = require('fs'); var util = require('util'); var path = require('path'); var mimeTypes = require('mime-types'); var console = require('console'); var arangodb = require('@arangodb'); var FoxxManager = require('@arangodb/foxx/manager'); var shallowCopy = require('@arangodb/util').shallowCopy; const MIME_DEFAULT = 'text/plain; charset=utf-8'; // // @brief current routing tree // // The route tree contains the URL hierarchy. In order to find a handler for a // request, you can traverse this tree. You need to backtrack if you handler // issues a `next()`. // // In order to speed up URL matching and next-finding, the tree is converted in // to a flat list, such that the least-specific middleware method comes // first. Then all other middleware methods with increasing specificity // followed by the most-specific method. Then all other methods with decreasing // specificity. // // Note that the object first on an entry for each database. // // { // _system: { // middleware: [ ... ], // routes: [ ... ], // ... // } // } // var RoutingTree = {}; // // @brief current routing list // // The flattened routing tree. The methods `flattenRoutingTree` takes a routing // tree and returns a routing list. // // Note that the object first contains an entry for each database, then an // entry for each method (like `GET`). // // { // _system: { // GET: [ ... ], // POST: [ ... ], // ... // } // } // var RoutingList = {}; // // @brief all methods // var ALL_METHODS = [ 'DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'PATCH' ]; var BODYFREE_METHODS = [ 'CONNECT', 'DELETE', 'GET', 'HEAD', 'TRACE' ]; // // @brief function that's returned for non-implemented actions // function notImplementedFunction (route, message) { 'use strict'; message += '\nThis error was triggered by the following route: ' + route.name; console.errorLines(message); return function (req, res, options, next) { res.responseCode = exports.HTTP_NOT_IMPLEMENTED; res.contentType = 'text/plain'; res.body = message; }; } // // @brief creates a callback for a callback action given as string // function createCallbackFromActionCallbackString (callback, parentModule, route) { 'use strict'; var actionModule = new Module(route.name, parentModule); try { actionModule._compile(`module.exports = ${callback}`, route.name); } catch (e) { console.errorStack(e); return notImplementedFunction(route, util.format( "could not generate callback for '%s'", callback )); } return actionModule.exports; } // // @brief looks up a callback for a callback action given as string // function lookupCallbackActionCallbackString (route, action, actionModule) { 'use strict'; var func; try { func = createCallbackFromActionCallbackString(action.callback, actionModule, route); } catch (err) { func = errorFunction(route, util.format( "an error occurred constructing callback for '%s': %s", action.callback, String(err.stack || err) )); } return { controller: func, options: action.options || {}, methods: action.methods || ALL_METHODS }; } // // @brief looks up a callback for a callback action // function lookupCallbackActionCallback (route, action, actionModule) { 'use strict'; if (typeof action.callback === 'string') { return lookupCallbackActionCallbackString(route, action, actionModule); } if (typeof action.callback === 'function') { return { controller: action.callback, options: action.options || {}, methods: action.methods || ALL_METHODS }; } return { controller: errorFunction(route, util.format( "an error occurred while generating callback '%s': unknown type '%s'", action.callback, typeof action.callback )), options: action.options || {}, methods: action.methods || ALL_METHODS }; } // // @brief returns module or error function // function requireModule (name, route, parentModule) { 'use strict'; try { return { module: parentModule.require(name) }; } catch (err) { if (err instanceof internal.ArangoError && err.errorNum === internal.errors.ERROR_MODULE_NOT_FOUND.code) { return notImplementedFunction(route, util.format( "an error occurred while loading the action module '%s': %s", name, String(err.stack || err) )); } else { return errorFunction(route, util.format( "an error occurred while loading action module '%s': %s", name, String(err.stack || err) )); } } } // // @brief returns function in module or error function // function moduleFunction (actionModule, name, route) { 'use strict'; var func = actionModule[name]; var type = typeof func; if (type !== 'function') { func = errorFunction(route, util.format( "invalid definition ('%s') for '%s' action in action module '%s'", type, name, actionModule.id )); } return func; } // // @brief looks up a callback for a module action // function lookupCallbackActionDo (route, action, parentModule) { 'use strict'; var path = action['do'].split('/'); var funcName = path.pop(); var moduleName = path.join('/'); var actionModule = requireModule(moduleName, route, parentModule); // module not found or load error if (typeof actionModule === 'function') { return { controller: func, options: action.options || {}, methods: action.methods || ALL_METHODS }; } // module found, extract function actionModule = actionModule.module; var func; if (actionModule.hasOwnProperty(funcName)) { func = moduleFunction(actionModule, funcName, route); } else { func = notImplementedFunction(route, util.format( "could not find action named '%s' in module '%s'", funcName, moduleName )); } return { controller: func, options: action.options || {}, methods: action.methods || ALL_METHODS }; } // // @brief creates the callback for a controller action // function createCallbackActionController (actionModule, route) { 'use strict'; return function (req, res, options, next) { var m = req.requestType.toLowerCase(); if (!actionModule.hasOwnProperty(m)) { m = 'do'; } if (actionModule.hasOwnProperty(m)) { return moduleFunction(actionModule, m, route)(req, res, options, next); } next(); }; } // // @brief looks up a callback for a controller action // function lookupCallbackActionController (route, action, parentModule) { 'use strict'; var actionModule = requireModule(action.controller, route, parentModule); if (typeof actionModule === 'function') { return { controller: actionModule, options: action.options || {}, methods: action.methods || ALL_METHODS }; } return { controller: createCallbackActionController(actionModule.module, route), options: action.options || {}, methods: action.methods || ALL_METHODS }; } // // @brief looks up a callback for a prefix controller action // function lookupCallbackActionPrefixController (route, action, parentModule) { 'use strict'; var prefix = action.prefixController; return { controller: function (req, res, options, next) { // determine path var path; if (req.hasOwnProperty('suffix')) { path = prefix + '/' + req.suffix.join('/'); } else { path = prefix; } // load module var actionModule = requireModule(path, route, parentModule); if (typeof actionModule === 'function') { actionModule(req, res, options, next); } else { createCallbackActionController(actionModule.module, route)(req, res, options, next); } }, options: action.options || {}, methods: action.methods || ALL_METHODS }; } // // @brief looks up a callback for an action // function lookupCallbackAction (route, action, context) { 'use strict'; // ............................................................................. // short-cut for prefix controller // ............................................................................. if (typeof action === 'string') { return lookupCallbackAction(route, { prefixController: action }); } // ............................................................................. // user-defined function // ............................................................................. if (action.hasOwnProperty('callback')) { return lookupCallbackActionCallback(route, action, context.module); } // ............................................................................. // function from module // ............................................................................. if (action.hasOwnProperty('do')) { return lookupCallbackActionDo(route, action, context.module); } // ............................................................................. // controller module // ............................................................................. if (action.hasOwnProperty('controller')) { return lookupCallbackActionController(route, action, context.module); } // ............................................................................. // prefix controller // ............................................................................. if (action.hasOwnProperty('prefixController')) { return lookupCallbackActionPrefixController(route, action, context.module); } return null; } // // @brief creates a callback for static data // function createCallbackStatic (code, type, body) { return function (req, res, options, next) { res.responseCode = code; res.contentType = type; res.body = body; }; } // // @brief looks up a callback for static data // function lookupCallbackStatic (content) { 'use strict'; var type; var body; var methods; var options; if (typeof content === 'string') { type = 'text/plain'; body = content; methods = [ exports.GET, exports.HEAD ]; options = {}; } else { type = content.contentType || 'text/plain'; body = content.body || ''; methods = content.methods || [ exports.GET, exports.HEAD ]; options = content.options || {}; } return { controller: createCallbackStatic(exports.HTTP_OK, type, body), options: options, methods: methods }; } // // @brief looks up a callback // // A callback is a structure with `controller`, `options`, and `methods`. // // { // controller: function (req, res, options, next) ..., // options: { ... }, // methods: [ ... ] // } // // The controller contains the function to call. `req` is the request, `res` // holds the response, `options` is the options array above and `methods` is a // list of methods to which the controller applies. // function lookupCallback (route, context) { 'use strict'; if (route.hasOwnProperty('content')) { return lookupCallbackStatic(route.content); } else if (route.hasOwnProperty('action')) { return lookupCallbackAction(route, route.action, context); } return null; } // // @brief splits a URL into parts // function splitUrl (url) { 'use strict'; var cleaned; var i; var parts; var re1; var re2; var ors; re1 = /^(:[a-zA-Z]+)*$/; re2 = /^(:[a-zA-Z]+)\?$/; parts = url.split('/'); cleaned = []; for (i = 0; i < parts.length; i++) { var part = parts[i]; if (part !== '' && part !== '.') { if (part === '..') { cleaned.pop(); } else if (part === '*') { cleaned.push({ prefix: true }); } else if (re1.test(part)) { ors = [ part.substr(1) ]; cleaned.push({ parameters: ors }); } else if (re2.test(part)) { ors = [ part.substr(1, part.length - 2) ]; cleaned.push({ parameters: ors, optional: true }); } else { cleaned.push(part); } } } return cleaned; } // // @brief looks up an URL // // Splits an URL into // // { // urlParts: [ ... ], // methods: [ ... ], // constraints: {} // } // function lookupUrl (prefix, url) { 'use strict'; if (url === undefined || url === '') { return null; } if (typeof url === 'string') { return { urlParts: splitUrl(prefix + '/' + url), methods: [ exports.GET, exports.HEAD ], constraint: {} }; } if (url.hasOwnProperty('match')) { return { urlParts: splitUrl(prefix + '/' + url.match), methods: url.methods || ALL_METHODS, constraint: url.constraint || {} }; } return null; } // // @brief defines a new route part // function defineRoutePart (storage, route, parts, pos, constraint, callback) { 'use strict'; var i; var part = parts[pos]; // otherwise we'll get an exception below if (part === undefined) { part = ''; } // ............................................................................. // exact match // ............................................................................. if (typeof part === 'string') { if (!storage.hasOwnProperty('exact')) { storage.exact = {}; } if (!storage.exact.hasOwnProperty(part)) { storage.exact[part] = {}; } var subpart = storage.exact[part]; if (pos + 1 < parts.length) { defineRoutePart(subpart, route, parts, pos + 1, constraint, callback); } else { if (!subpart.hasOwnProperty('routes')) { subpart.routes = []; } subpart.routes.push({ route: route, priority: route.priority || 0, callback: callback }); } } // ............................................................................. // parameter match // ............................................................................. else if (part.hasOwnProperty('parameters')) { if (!storage.hasOwnProperty('parameters')) { storage.parameters = []; } var ok = true; if (part.optional) { if (pos + 1 < parts.length) { console.error("cannot define optional parameter within url, ignoring route '%s'", route.name); ok = false; } else if (part.parameters.length !== 1) { console.error("cannot define multiple optional parameters, ignoring route '%s'", route.name); ok = false; } } if (ok) { for (i = 0; i < part.parameters.length; i++) { var p = part.parameters[i]; var subsub = { parameter: p, match: {} }; storage.parameters.push(subsub); if (constraint.hasOwnProperty(p)) { subsub.constraint = constraint[p]; } subsub.optional = part.optional || false; if (pos + 1 < parts.length) { defineRoutePart(subsub.match, route, parts, pos + 1, constraint, callback); } else { var match = subsub.match; if (!match.hasOwnProperty('routes')) { match.routes = []; } match.routes.push({ route: route, priority: route.priority || 0, callback: callback }); } } } } // ............................................................................. // prefix match // ............................................................................. else if (part.hasOwnProperty('prefix')) { if (!storage.hasOwnProperty('prefix')) { storage.prefix = {}; } if (pos + 1 < parts.length) { console.error("cannot define prefix match within url, ignoring route '%s'", route.name); } else { var subprefix = storage.prefix; if (!subprefix.hasOwnProperty('routes')) { subprefix.routes = []; } subprefix.routes.push({ route: route, priority: route.priority || 0, callback: callback }); } } } // // @brief intersect methods // function intersectMethods (a, b) { 'use strict'; var d = {}; var i; var j; var results = []; a = a || ALL_METHODS; b = b || ALL_METHODS; for (i = 0; i < b.length; i++) { d[b[i].toUpperCase()] = true; } for (j = 0; j < a.length; j++) { var name = a[j].toUpperCase(); if (d[name]) { results.push(name); } } return results; } // // @brief defines a new route // function defineRoute (storage, route, url, callback) { 'use strict'; var j; var methods = intersectMethods(url.methods, callback.methods); for (j = 0; j < methods.length; j++) { var method = methods[j]; defineRoutePart(storage[method], route, url.urlParts, 0, url.constraint, callback); } } // // @brief installs a new route in a tree // function installRoute (storage, route, urlPrefix, context) { 'use strict'; var url = lookupUrl(urlPrefix, route.url); if (url === null) { console.error("route has an unknown url, ignoring '%s'", route.name); return; } var callback = lookupCallback(route, context); if (callback === null) { console.error("route has an unknown callback, ignoring '%s'", route.name); return; } defineRoute(storage, route, url, callback); } // // @brief creates additional routing information // function analyseRoutes (storage, routes) { 'use strict'; var j; var i; var urlPrefix = routes.urlPrefix || ''; // use normal root module or app context var foxxContext = routes.foxxContext || { module: new Module(routes.name || 'anonymous') }; // install the routes var keys = [ 'routes', 'middleware' ]; for (j = 0; j < keys.length; j++) { var key = keys[j]; if (routes.hasOwnProperty(key)) { var r = routes[key]; for (i = 0; i < r.length; i++) { installRoute(storage[key], r[i], urlPrefix, foxxContext); } } } } // // @brief builds a routing tree // function buildRoutingTree (routes) { 'use strict'; var j; var storage = { routes: {}, middleware: {} }; for (j = 0; j < ALL_METHODS.length; j++) { var method = ALL_METHODS[j]; storage.routes[method] = {}; storage.middleware[method] = {}; } for (j = 0; j < routes.length; j++) { var route = routes[j]; try { if (route.hasOwnProperty('routes') || route.hasOwnProperty('middleware')) { analyseRoutes(storage, route); } else { // use normal root module or app context var foxxContext = routes.foxxContext || { module: new Module(route.name || 'anonymous') }; installRoute(storage.routes, route, '', foxxContext); } } catch (err) { console.errorLines("cannot install route '%s': %s", route.name, String(err.stack || err)); } } return storage; } // // @brief flattens the routing tree for either middleware or normal methods // function flattenRouting (routes, path, rexpr, urlParameters, depth, prefix) { 'use strict'; var cur; var i; var k; var newUrlParameters; var parameter; var match; var result = []; // ............................................................................. // descend down to the next exact matches // ............................................................................. if (routes.hasOwnProperty('exact')) { for (k in routes.exact) { if (routes.exact.hasOwnProperty(k)) { result = result.concat(flattenRouting( routes.exact[k], path + '/' + k, rexpr + '/' + k.replace(/([\.\+\*\?\^\$\(\)\[\]])/g, '\\$1'), Object.assign({}, urlParameters), depth + 1, false)); } } } // ............................................................................. // descend down the parameter matches // ............................................................................. if (routes.hasOwnProperty('parameters')) { for (i = 0; i < routes.parameters.length; i++) { parameter = routes.parameters[i]; if (parameter.hasOwnProperty('constraint')) { var constraint = parameter.constraint; var pattern = /\/.*\//; if (pattern.test(constraint)) { match = '/' + constraint.substr(1, constraint.length - 2); } else { match = '/' + constraint; } } else { match = '/[^/]+'; } if (parameter.optional) { cur = rexpr + '(' + match + ')?'; } else { cur = rexpr + match; } newUrlParameters = Object.assign({}, urlParameters); newUrlParameters[parameter.parameter] = depth; result = result.concat(flattenRouting( parameter.match, path + '/:' + parameter.parameter + (parameter.optional ? '?' : ''), cur, newUrlParameters, depth + 1, false)); } } // ............................................................................. // we have done the depth first descend, now add the current callback(s) // ............................................................................. if (routes.hasOwnProperty('routes')) { var sorted = [...routes.routes.sort(function (a, b) { return b.priority - a.priority; })]; for (i = 0; i < sorted.length; i++) { sorted[i] = { path: path, regexp: new RegExp('^' + rexpr + '$'), prefix: prefix, depth: depth, urlParameters: urlParameters, callback: sorted[i].callback, route: sorted[i].route }; } result = result.concat(sorted); } // ............................................................................. // finally append the prefix matches // ............................................................................. if (routes.hasOwnProperty('prefix')) { result = result.concat(flattenRouting( routes.prefix, path + '/*', rexpr + '(/[^/]+)*/?', shallowCopy(urlParameters), depth + 1, true)); } return result; } // // @brief flattens the routing tree complete // function flattenRoutingTree (tree) { 'use strict'; var i; var flat = {}; for (i = 0; i < ALL_METHODS.length; i++) { var method = ALL_METHODS[i]; var a = flattenRouting(tree.routes[method], '', '', {}, 0, false); var b = flattenRouting(tree.middleware[method], '', '', {}, 0, false); flat[method] = b.reverse().concat(a); } return flat; } // // @brief creates the Foxx routing actions // function foxxRouting (req, res, options, next) { var mount = options.mount; try { const service = FoxxManager.lookupService(mount); if (service.isDevelopment || !options.routing) { delete options.error; if (service.isDevelopment) { FoxxManager.reloadInstalledService(mount, true); } options.routing = flattenRoutingTree(buildRoutingTree([ FoxxManager.ensureRouted(mount).routes ])); } } catch (e) { console.errorStack(e, `Failed to load Foxx service mounted at "${mount}"`); options.error = { code: exports.HTTP_SERVICE_UNAVAILABLE, num: e.errorNum || arangodb.ERROR_HTTP_SERVICE_UNAVAILABLE, msg: `Failed to load Foxx service mounted at "${mount}"` }; } if (options.error) { exports.resultError(req, res, options.error.code, options.error.num, options.error.msg, {}, options.error.info); } else { try { routeRequest(req, res, options.routing); } catch (err2) { resultException( req, res, err2, {}, "failed to execute Foxx service mounted at '" + mount + "'"); } } } // // @brief flushes cache and reload routing information // function buildRouting (dbname) { 'use strict'; // compute all routes var routes = []; var routing = arangodb.db._collection('_routing'); if (routing !== null) { let i = routing.all(); while (i.hasNext()) { var n = i.next(); var c = Object.assign({}, n); c.name = '_routing.document("' + c._key + '")'; routes.push(c); } } // allow the collection to unload routing = null; // install the Foxx routes var mountPoints = FoxxManager._mountPoints(); for (let i = 0; i < mountPoints.length; i++) { var mountPoint = mountPoints[i]; routes.push({ name: "Foxx service mounted at '" + mountPoint + "'", url: { match: mountPoint + '/*' }, action: { callback: foxxRouting, options: { mount: mountPoint } } }); } // build the routing tree RoutingTree[dbname] = buildRoutingTree(routes); // compute the flat routes RoutingList[dbname] = flattenRoutingTree(RoutingTree[dbname]); } // // @brief finds the next routing // function nextRouting (state) { 'use strict'; var i; var k; for (i = state.position + 1; i < state.routing.length; i++) { var route = state.routing[i]; if (route.regexp.test(state.url)) { state.position = i; state.route = route; if (route.prefix) { state.prefix = '/' + state.parts.slice(0, route.depth - 1).join('/'); state.suffix = state.parts.slice(route.depth - 1, state.parts.length); state.rawSuffix = state.rawParts.slice(route.depth - 1, state.rawParts.length); } else { delete state.prefix; delete state.suffix; delete state.rawSuffix; } state.urlParameters = {}; if (route.urlParameters) { for (k in route.urlParameters) { if (route.urlParameters.hasOwnProperty(k)) { state.urlParameters[k] = state.parts[route.urlParameters[k]]; } } } return state; } } delete state.route; delete state.prefix; delete state.suffix; delete state.rawSuffix; state.urlParameters = {}; return state; } // // @brief finds the first routing // function firstRouting (type, parts, routes, rawParts) { 'use strict'; var url = '/' + parts.join('/'); if (!routes || !routes.hasOwnProperty(type)) { return { position: -1, urlParameters: {}, parts, rawParts, url }; } return nextRouting({ routing: routes[type], position: -1, urlParameters: {}, parts, rawParts, url }); } // // @brief routing function // function routeRequest (req, res, routes) { if (routes === undefined) { var dbname = arangodb.db._name(); if (!RoutingList[dbname]) { buildRouting(dbname); } routes = RoutingList[dbname]; } var action = firstRouting(req.requestType, req.suffix, routes, req.rawSuffix); function execute () { if (action.route === undefined) { resultNotFound(req, res, arangodb.ERROR_HTTP_NOT_FOUND, "unknown path '" + req.url + "'"); return; } if (action.route.path !== undefined) { req.path = action.route.path; } else { delete req.path; } if (action.prefix !== undefined) { req.prefix = action.prefix; } else { delete req.prefix; } if (action.suffix !== undefined) { req.suffix = action.suffix; } else { delete req.suffix; } if (action.rawSuffix !== undefined) { req.rawSuffix = action.rawSuffix; } else { delete req.rawSuffix; } if (action.urlParameters !== undefined) { req.urlParameters = action.urlParameters; } else { req.urlParameters = {}; } if (req.path) { var path = req.path; req.path = function (params, suffix) { var p = path; params = params || req.urlParameters; Object.keys(params).forEach(function (key) { p = p.replace(new RegExp(':' + key + '\\??', 'g'), params[key]); }); p = p.replace(/:[a-zA-Z]+\?$/, ''); suffix = suffix || req.suffix; if (Array.isArray(suffix)) { suffix = suffix.join('/'); } return p.replace(/\*$/, suffix || ''); }; } var func = action.route.callback.controller; if (func === null || typeof func !== 'function') { func = errorFunction(action.route, 'Invalid callback definition found for route ' + JSON.stringify(action.route) + ' while searching for callback.controller'); } try { func(req, res, action.route.callback.options, next); } catch (err) { if (!err.hasOwnProperty('route')) { err.route = action.route; } throw err; } } function next (restart) { if (restart) { routeRequest(req, res); } else { action = nextRouting(action); execute(); } } execute(); } // // @brief flushes the routing cache // function reloadRouting () { 'use strict'; RoutingTree = {}; RoutingList = {}; FoxxManager._resetCache(); } // // @brief loads all actions // function startup () { if (internal.defineAction === undefined) { return; } var actionPath = fs.join(internal.startupPath, 'actions'); var actions = fs.listTree(actionPath); var i; for (i = 0; i < actions.length; i++) { var file = actions[i]; var full = fs.join(actionPath, file); if (full !== '' && fs.isFile(full)) { if (file.match(/.*\.js$/)) { var content = fs.read(full); content = '(function () {\n' + content + '\n}());'; internal.executeScript(content, undefined, full); } } } } // // @brief was docuBlock actionsDefineHttp // function defineHttp (options) { 'use strict'; var url = options.url; var callback = options.callback; if (typeof callback !== 'function') { console.error("callback for '%s' must be a function, got '%s'", url, typeof callback); return; } let parameters = { prefix: true, allowUseDatabase: false, isSystem: false }; if (options.hasOwnProperty('isSystem')) { parameters.isSystem = options.isSystem; } if (options.hasOwnProperty('prefix')) { parameters.prefix = options.prefix; } // currently only used by api-cluster.js if (options.hasOwnProperty('allowUseDatabase')) { parameters.allowUseDatabase = options.allowUseDatabase; } try { internal.defineAction(url, callback, parameters); } catch (e) { console.errorLines("action '%s' encountered error:\n%s", url, e.stack || String(e)); } } // // @brief add a cookie // function addCookie (res, name, value, lifeTime, path, domain, secure, httpOnly) { 'use strict'; if (name === undefined) { return; } if (value === undefined) { return; } var cookie = { 'name': name, 'value': value }; if (lifeTime !== undefined && lifeTime !== null) { cookie.lifeTime = parseInt(lifeTime, 10); } if (path !== undefined && path !== null) { cookie.path = path; } if (domain !== undefined && domain !== null) { cookie.domain = domain; } if (secure !== undefined && secure !== null) { cookie.secure = (secure) ? true : false; } if (httpOnly !== undefined && httpOnly !== null) { cookie.httpOnly = (httpOnly) ? true : false; } if (res.cookies === undefined || res.cookies === null) { res.cookies = []; } res.cookies.push(cookie); } // // @brief was docuBlock actionsGetErrorMessage // function getErrorMessage (code) { 'use strict'; var key; for (key in internal.errors) { if (internal.errors.hasOwnProperty(key)) { if (internal.errors[key].code === code) { return internal.errors[key].message; } } } return ''; } // // @brief extracts the body as json // function getJsonBody (req, res, code) { 'use strict'; var body; var err; try { body = JSON.parse(req.requestBody || '{}') || {}; } catch (err1) { resultBad(req, res, arangodb.ERROR_HTTP_CORRUPTED_JSON, err1); return undefined; } if (!body || !(body instanceof Object)) { if (code === undefined) { code = arangodb.ERROR_HTTP_CORRUPTED_JSON; } err = new internal.ArangoError(); err.errorNum = code; err.errorMessage = 'expecting a valid JSON object as body'; resultBad(req, res, code, err); return undefined; } return body; } // // @brief was docuBlock actionsResultError // function resultError (req, res, httpReturnCode, errorNum, errorMessage, headers, keyvals) { 'use strict'; var i; var msg; res.responseCode = httpReturnCode; res.contentType = 'application/json; charset=utf-8'; if (typeof errorNum === 'string') { keyvals = headers; headers = errorMessage; errorMessage = errorNum; errorNum = httpReturnCode; } if (errorMessage === undefined || errorMessage === null) { msg = getErrorMessage(errorNum); } else { msg = String(errorMessage); } var result = {}; if (keyvals !== undefined) { for (i in keyvals) { if (keyvals.hasOwnProperty(i)) { result[i] = keyvals[i]; } } } result.error = true; result.code = httpReturnCode; result.errorNum = errorNum; result.errorMessage = msg; res.body = JSON.stringify(result); if (headers !== undefined && headers !== null) { res.headers = headers; } } // // @brief function that's returned for actions that produce an error // function errorFunction (route, message) { 'use strict'; message += '\nThis error was triggered by the following route ' + JSON.stringify(route); console.error('%s', message); return function (req, res, options, next) { res.responseCode = exports.HTTP_SERVER_ERROR; res.contentType = 'text/plain'; res.body = message; }; } // // @brief convenience function for a bad parameter // function badParameter (req, res, name) { 'use strict'; resultError(req, res, exports.HTTP_BAD, exports.HTTP_BAD, util.format( "invalid value for parameter '%s'", name )); } // // @brief was docuBlock actionsResultOk // function resultOk (req, res, httpReturnCode, result, headers) { 'use strict'; res.responseCode = httpReturnCode; res.contentType = 'application/json; charset=utf-8'; // add some default attributes to result if (result === undefined) { result = {}; } // check the type of the result. if (typeof result !== 'string' && typeof result !== 'number' && typeof result !== 'boolean') { // only modify result properties if none of the above types // otherwise the strict mode will throw result.error = false; result.code = httpReturnCode; } res.body = JSON.stringify(result); if (headers !== undefined) { res.headers = headers; } } // // @brief was docuBlock actionsResultBad // function resultBad (req, res, code, msg, headers) { 'use strict'; resultError(req, res, exports.HTTP_BAD, code, msg, headers); } // // @brief was docuBlock actionsResultNotFound // function resultNotFound (req, res, code, msg, headers) { 'use strict'; resultError(req, res, exports.HTTP_NOT_FOUND, code, msg, headers); } // // @brief was docuBlock actionsResultNotImplemented // function resultNotImplemented (req, res, msg, headers) { 'use strict'; resultError(req, res, exports.HTTP_NOT_IMPLEMENTED, arangodb.ERROR_NOT_IMPLEMENTED, msg, headers); } // // @brief was docuBlock actionsResultUnsupported // function resultUnsupported (req, res, headers) { 'use strict'; resultError(req, res, exports.HTTP_METHOD_NOT_ALLOWED, arangodb.ERROR_HTTP_METHOD_NOT_ALLOWED, 'Unsupported method', headers); } // // @brief internal function for handling redirects // function handleRedirect (req, res, options, headers) { var destination; var url = ''; destination = options.destination; if (destination.substr(0, 5) !== 'http:' && destination.substr(0, 6) !== 'https:') { if (options.relative) { var u = req.url; if (0 < u.length && u[u.length - 1] === '/') { url += '/_db/' + encodeURIComponent(req.database) + u + destination; } else { url += '/_db/' + encodeURIComponent(req.database) + u + '/' + destination; } } else { url += destination; } } else { url = destination; } res.contentType = 'text/html'; res.body = '
This page has moved to ' + url + '.
'; if (headers !== undefined) { res.headers = shallowCopy(headers); } else { res.headers = {}; } res.headers.location = url; } // // @brief was docuBlock actionsResultPermanentRedirect // function resultPermanentRedirect (req, res, options, headers) { 'use strict'; res.responseCode = exports.HTTP_MOVED_PERMANENTLY; handleRedirect(req, res, options, headers); } // // @brief was docuBlock actionsResultTemporaryRedirect // function resultTemporaryRedirect (req, res, options, headers) { 'use strict'; res.responseCode = exports.HTTP_TEMPORARY_REDIRECT; handleRedirect(req, res, options, headers); } // // @brief function describing a cursor // function resultCursor (req, res, cursor, code, options) { 'use strict'; var rows; var count; var hasCount; var hasNext; var cursorId; var extra; if (Array.isArray(cursor)) { // performance optimization: if the value passed in is an array, we can // use it as it is hasCount = ((options && options.countRequested) ? true : false); count = cursor.length; rows = cursor; hasNext = false; cursorId = null; } else if (typeof cursor === 'object' && cursor.hasOwnProperty('json')) { // cursor is a regular JS object (performance optimization) hasCount = Boolean(options && options.countRequested); count = cursor.json.length; rows = cursor.json; extra = {}; [ 'stats', 'warnings', 'profile' ].forEach(function (d) { if (cursor.hasOwnProperty(d)) { extra[d] = cursor[d]; } }); hasNext = false; cursorId = null; } else if (typeof cursor === 'string' && cursor.match(/^\d+$/)) { // cursor is assumed to be a cursor id var tmp = JSON_CURSOR(cursor); hasCount = true; count = tmp.count; rows = tmp.result; hasNext = tmp.hasOwnProperty('id'); extra = undefined; if (hasNext) { cursorId = tmp.id; } else { cursorId = null; } } var result = { result: rows, hasMore: hasNext }; if (cursorId) { result.id = cursorId; } if (hasCount) { result.count = count; } if (extra !== undefined && Object.keys(extra).length > 0) { result.extra = extra; } if (code === undefined) { code = exports.HTTP_CREATED; } resultOk(req, res, code, result); } // // @brief was docuBlock actionsCollectionNotFound // function collectionNotFound (req, res, collection, headers) { 'use strict'; if (collection === undefined) { resultError(req, res, exports.HTTP_BAD, arangodb.ERROR_HTTP_BAD_PARAMETER, 'expecting a collection name or identifier', headers); } else { resultError(req, res, exports.HTTP_NOT_FOUND, arangodb.ERROR_ARANGO_DATA_SOURCE_NOT_FOUND, "unknown collection '" + collection + "'", headers); } } // // @brief was docuBlock actionsIndexNotFound // function indexNotFound (req, res, collection, index, headers) { 'use strict'; if (collection === undefined) { resultError(req, res, exports.HTTP_BAD, arangodb.ERROR_HTTP_BAD_PARAMETER, 'expecting a collection name or identifier', headers); } else if (index === undefined) { resultError(req, res, exports.HTTP_BAD, arangodb.ERROR_HTTP_BAD_PARAMETER, 'expecting an index identifier', headers); } else { resultError(req, res, exports.HTTP_NOT_FOUND, arangodb.ERROR_ARANGO_INDEX_NOT_FOUND, "unknown index '" + index + "'", headers); } } function arangoErrorToHttpCode (num) { if (!num) { return exports.HTTP_SERVER_ERROR; } switch (num) { case arangodb.ERROR_INTERNAL: case arangodb.ERROR_OUT_OF_MEMORY: case arangodb.ERROR_GRAPH_TOO_MANY_ITERATIONS: case arangodb.ERROR_ARANGO_DOCUMENT_KEY_BAD: return exports.HTTP_SERVER_ERROR; case arangodb.ERROR_FORBIDDEN: case arangodb.ERROR_ARANGO_USE_SYSTEM_DATABASE: return exports.HTTP_FORBIDDEN; case arangodb.ERROR_ARANGO_DATA_SOURCE_NOT_FOUND: case arangodb.ERROR_ARANGO_DOCUMENT_NOT_FOUND: case arangodb.ERROR_ARANGO_DATABASE_NOT_FOUND: case arangodb.ERROR_ARANGO_NO_INDEX: case arangodb.ERROR_ARANGO_INDEX_NOT_FOUND: case arangodb.ERROR_CURSOR_NOT_FOUND: case arangodb.ERROR_USER_NOT_FOUND: case arangodb.ERROR_TASK_NOT_FOUND: case arangodb.ERROR_QUERY_NOT_FOUND: case arangodb.ERROR_SERVICE_NOT_FOUND: return exports.HTTP_NOT_FOUND; case arangodb.ERROR_REQUEST_CANCELED: return exports.HTTP_REQUEST_TIMEOUT; case arangodb.ERROR_ARANGO_DUPLICATE_NAME: case arangodb.ERROR_ARANGO_DUPLICATE_IDENTIFIER: case arangodb.ERROR_USER_DUPLICATE: case arangodb.ERROR_GRAPH_DUPLICATE: case arangodb.ERROR_TASK_DUPLICATE_ID: case arangodb.ERROR_SERVICE_MOUNTPOINT_CONFLICT: return exports.HTTP_CONFLICT; case arangodb.ERROR_CLUSTER_UNSUPPORTED: return exports.HTTP_NOT_IMPLEMENTED; case arangodb.ERROR_ARANGO_ILLEGAL_NAME: case arangodb.ERROR_ARANGO_UNIQUE_CONSTRAINT_VIOLATED: case arangodb.ERROR_ARANGO_CROSS_COLLECTION_REQUEST: case arangodb.ERROR_ARANGO_INDEX_HANDLE_BAD: case arangodb.ERROR_ARANGO_DOCUMENT_TOO_LARGE: case arangodb.ERROR_ARANGO_COLLECTION_NOT_UNLOADED: case arangodb.ERROR_ARANGO_COLLECTION_TYPE_INVALID: case arangodb.ERROR_ARANGO_ATTRIBUTE_PARSER_FAILED: case arangodb.ERROR_ARANGO_DOCUMENT_KEY_UNEXPECTED: case arangodb.ERROR_ARANGO_DOCUMENT_KEY_MISSING: case arangodb.ERROR_ARANGO_DOCUMENT_TYPE_INVALID: case arangodb.ERROR_ARANGO_DATABASE_NAME_INVALID: case arangodb.ERROR_ARANGO_INVALID_KEY_GENERATOR: case arangodb.ERROR_ARANGO_INVALID_EDGE_ATTRIBUTE: case arangodb.ERROR_ARANGO_COLLECTION_TYPE_MISMATCH: case arangodb.ERROR_ARANGO_COLLECTION_NOT_LOADED: case arangodb.ERROR_ARANGO_DOCUMENT_REV_BAD: return exports.HTTP_BAD; case arangodb.ERROR_CLUSTER_BACKEND_UNAVAILABLE: case arangodb.ERROR_CLUSTER_SHARD_LEADER_RESIGNED: return exports.HTTP_SERVICE_UNAVAILABLE; case arangodb.ERROR_CLUSTER_SHARD_LEADER_REFUSES_REPLICATION: case arangodb.ERROR_CLUSTER_SHARD_FOLLOWER_REFUSES_OPERATION: return exports.HTTP_NOT_ACCEPTABLE; } return exports.HTTP_BAD; } // // @brief was docuBlock actionsResultException // function resultException (req, res, err, headers, verbose) { 'use strict'; var code; var msg = err.message; var num; var info = {}; if (verbose !== false) { console.errorStack(err); if (typeof verbose === 'string') { msg = verbose; } } if (err instanceof internal.ArangoError) { if (err.errorMessage !== '') { if (verbose !== false) { info.message = err.errorMessage; } if (typeof verbose !== 'string') { msg = err.errorMessage; } } num = err.errorNum || arangodb.ERROR_INTERNAL; code = arangoErrorToHttpCode(num); } else if (err instanceof TypeError) { num = arangodb.ERROR_TYPE_ERROR; code = exports.HTTP_BAD; } else if (err.statusCode) { num = err.statusCode; code = err.statusCode; } else { num = arangodb.ERROR_HTTP_SERVER_ERROR; code = exports.HTTP_SERVER_ERROR; } resultError(req, res, code, num, msg, headers, info); } // // @brief creates a simple post callback // function easyPostCallback (opts) { 'use strict'; return function (req, res) { var body = getJsonBody(req, res); if (opts.body === true && body === undefined) { return; } try { var result = opts.callback(body, req); resultOk(req, res, exports.HTTP_OK, result); } catch (err) { resultException(req, res, err, undefined, false); } }; } // // @brief echos a request // function echoRequest (req, res, options, next) { 'use strict'; var result = { request: req, options: options }; res.responseCode = exports.HTTP_OK; res.contentType = 'application/json; charset=utf-8'; res.body = JSON.stringify(result); } // // @brief logs a request // function logRequest (req, res, options, next) { 'use strict'; var log; var level; var token; if (options.hasOwnProperty('level')) { level = options.level; if (level === 'debug') { log = console.debug; } else if (level === 'error') { log = console.error; } else if (level === 'info') { log = console.info; } else if (level === 'log') { log = console.log; } else if (level === 'warn' || level === 'warning') { log = console.warn; } else { log = console.log; } } else { log = console.log; } if (options.hasOwnProperty('token')) { token = options.token; } else { token = '@(#' + internal.time() + ') '; } log('received request %s: %s', token, JSON.stringify(req)); next(); log('produced response %s: %s', token, JSON.stringify(res)); } // // @brief redirects a request // function redirectRequest (req, res, options, next) { 'use strict'; if (options.permanently) { resultPermanentRedirect(req, res, options); } else { resultTemporaryRedirect(req, res, options); } } // // @brief redirects a request // function pathHandler (req, res, options, next) { 'use strict'; var filepath, root, filename, encodedFilename; filepath = req.suffix.length ? path.normalize(['', ...req.suffix.map((part) => part)].join(path.sep)) : ''; root = options.path; if (options.root) { root = fs.join(options.root, root); } filename = fs.join(root, filepath); encodedFilename = filename; if (fs.exists(filename)) { res.responseCode = exports.HTTP_OK; res.contentType = options.type || mimeTypes.lookup(filename) || MIME_DEFAULT; if (options.gzip === true) { // check if client is capable of gzip encoding if (req.headers) { var encoding = req.headers['accept-encoding']; if (encoding && encoding.indexOf('gzip') >= 0) { // check if gzip file is available encodedFilename = encodedFilename + '.gz'; if (fs.exists(encodedFilename)) { if (!res.headers) { res.headers = {}; } res.headers['Content-Encoding'] = 'gzip'; } } } } res.bodyFromFile = encodedFilename; } else { console.warn(`File not found "${req.url}": file "${filepath}" does not exist in "${root}".`); res.responseCode = exports.HTTP_NOT_FOUND; res.contentType = 'text/plain'; res.body = `cannot find file "${filepath}"`; } } // // @brief helper function to stringify a request // function stringifyRequest (req) { return req.requestType + ' ' + req.absoluteUrl(); } // load all actions from the actions directory exports.startup = startup; // only for debugging exports.buildRouting = buildRouting; exports.buildRoutingTree = buildRoutingTree; exports.flattenRoutingTree = flattenRoutingTree; exports.nextRouting = nextRouting; exports.firstRouting = function (method, parts, routes) { if (typeof parts === 'string') { parts = parts.split('/').filter(s => s !== ''); } return firstRouting(method, parts, routes, parts); }; // public functions exports.routingTree = () => RoutingTree; exports.routingList = () => RoutingList; exports.routeRequest = routeRequest; exports.defineHttp = defineHttp; exports.getErrorMessage = getErrorMessage; exports.getJsonBody = getJsonBody; exports.errorFunction = errorFunction; exports.reloadRouting = reloadRouting; exports.addCookie = addCookie; exports.stringifyRequest = stringifyRequest; exports.arangoErrorToHttpCode = arangoErrorToHttpCode; // standard HTTP responses exports.badParameter = badParameter; exports.resultBad = resultBad; exports.resultError = resultError; exports.resultNotFound = resultNotFound; exports.resultNotImplemented = resultNotImplemented; exports.resultOk = resultOk; exports.resultPermanentRedirect = resultPermanentRedirect; exports.resultTemporaryRedirect = resultTemporaryRedirect; exports.resultUnsupported = resultUnsupported; // ArangoDB specific responses exports.resultCursor = resultCursor; exports.collectionNotFound = collectionNotFound; exports.indexNotFound = indexNotFound; exports.resultException = resultException; // standard actions exports.easyPostCallback = easyPostCallback; exports.echoRequest = echoRequest; exports.logRequest = logRequest; exports.redirectRequest = redirectRequest; exports.pathHandler = pathHandler; // some useful constants exports.DELETE = 'DELETE'; exports.GET = 'GET'; exports.HEAD = 'HEAD'; exports.OPTIONS = 'OPTIONS'; exports.POST = 'POST'; exports.PUT = 'PUT'; exports.PATCH = 'PATCH'; exports.ALL_METHODS = ALL_METHODS; exports.BODYFREE_METHODS = BODYFREE_METHODS; // HTTP 2xx exports.HTTP_OK = 200; exports.HTTP_CREATED = 201; exports.HTTP_ACCEPTED = 202; exports.HTTP_PARTIAL = 203; exports.HTTP_NO_CONTENT = 204; // HTTP 3xx exports.HTTP_MOVED_PERMANENTLY = 301; exports.HTTP_FOUND = 302; exports.HTTP_SEE_OTHER = 303; exports.HTTP_NOT_MODIFIED = 304; exports.HTTP_TEMPORARY_REDIRECT = 307; // HTTP 4xx exports.HTTP_BAD = 400; exports.HTTP_UNAUTHORIZED = 401; exports.HTTP_PAYMENT = 402; exports.HTTP_FORBIDDEN = 403; exports.HTTP_NOT_FOUND = 404; exports.HTTP_METHOD_NOT_ALLOWED = 405; exports.HTTP_REQUEST_TIMEOUT = 408; exports.HTTP_CONFLICT = 409; exports.HTTP_PRECONDITION_FAILED = 412; exports.HTTP_ENTITY_TOO_LARGE = 413; exports.HTTP_I_AM_A_TEAPOT = 418; exports.HTTP_UNPROCESSABLE_ENTIT = 422; exports.HTTP_HEADER_TOO_LARGE = 431; // HTTP 5xx exports.HTTP_SERVER_ERROR = 500; exports.HTTP_NOT_IMPLEMENTED = 501; exports.HTTP_BAD_GATEWAY = 502; exports.HTTP_SERVICE_UNAVAILABLE = 503; exports.HTTP_GATEWAY_TIMEOUT = 504;