internals.implementation = function (server, options) { var settings = Hoek.cloneWithShallow(options, 'provider'); // Options can be reused // Lookup provider if (typeof settings.provider === 'object') { settings.name = 'custom'; } else { settings.name = settings.provider; settings.provider = Providers[settings.provider].call(null, settings.config) } Joi.assert(settings, internals.schema); // Setup cookie for managing temporary authorization state var cookieOptions = { encoding: 'iron', path: '/', password: settings.password, isSecure: settings.isSecure !== false, // Defaults to true isHttpOnly: settings.isHttpOnly !== false, // Defaults to true ttl: settings.ttl, domain: settings.domain, failAction: 'log', clearInvalid: true }; settings.cookie = settings.cookie || 'bell-' + settings.name; server.state(settings.cookie, cookieOptions); return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) }; };
internals.implementation = function (server, options) { let settings = Hoek.cloneWithShallow(options, 'provider'); // Options can be reused // Lookup provider if (typeof settings.provider === 'object') { settings.name = settings.provider.name || 'custom'; } else { settings.name = settings.provider; settings.provider = Providers[settings.provider].call(null, settings.config); } const results = Joi.validate(settings, internals.schema); Hoek.assert(!results.error, results.error); // Passed validation, use Joi converted settings settings = results.value; // Setup cookie for managing temporary authorization state const cookieOptions = { encoding: 'iron', path: '/', password: settings.password, isSecure: settings.isSecure !== false, // Defaults to true isHttpOnly: settings.isHttpOnly !== false, // Defaults to true isSameSite: false, ttl: settings.ttl, domain: settings.domain, ignoreErrors: true, clearInvalid: true }; settings.cookie = settings.cookie || 'bell-' + settings.name; try { server.state(settings.cookie, cookieOptions); } catch (exception) { /* $lab:coverage:off$ */ // This is to support Hapi 13.5.0 so that adding isSameSite: false option is not a breaking change if (exception.message.indexOf('isSameSite') === -1) { throw exception; } delete cookieOptions.isSameSite; server.state(settings.cookie, cookieOptions); /* $lab:coverage:on$ */ } if (internals.simulate) { return internals.simulated(settings); } return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) }; };
_add(name, method, options, realm) { Hoek.assert(typeof method === 'function', 'method must be a function'); Hoek.assert(typeof name === 'string', 'name must be a string'); Hoek.assert(name.match(internals.methodNameRx), 'Invalid name:', name); Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists:', name); options = Config.apply('method', options, name); const settings = Hoek.cloneWithShallow(options, ['bind']); settings.generateKey = settings.generateKey || internals.generateKey; const bind = settings.bind || realm.settings.bind || null; const bound = !bind ? method : (...args) => method.apply(bind, args); // Not cached if (!settings.cache) { return this._assign(name, bound); } // Cached Hoek.assert(!settings.cache.generateFunc, 'Cannot set generateFunc with method caching:', name); Hoek.assert(settings.cache.generateTimeout !== undefined, 'Method caching requires a timeout value in generateTimeout:', name); settings.cache.generateFunc = (id, flags) => bound(...id.args, flags); const cache = this.core._cachePolicy(settings.cache, '#' + name); const func = function (...args) { const key = settings.generateKey.apply(bind, args); if (typeof key !== 'string') { return Promise.reject(Boom.badImplementation('Invalid method key when invoking: ' + name, { name, args })); } return cache.get({ id: key, args }); }; func.cache = { drop: function (...args) { const key = settings.generateKey.apply(bind, args); if (typeof key !== 'string') { return Promise.reject(Boom.badImplementation('Invalid method key when invoking: ' + name, { name, args })); } return cache.drop(key); }, stats: cache.stats }; this._assign(name, func, func); }
manifest.servers.forEach(function (server) { if (server.host && server.host.indexOf('$env.') === 0) { server.host = process.env[server.host.slice(5)]; } if (server.port && typeof server.port === 'string' && server.port.indexOf('$env.') === 0) { server.port = parseInt(process.env[server.port.slice(5)], 10); } var serverOptions = server.options; if (serverOptions && serverOptions.views) { serverOptions = Hoek.cloneWithShallow(serverOptions, 'views'); serverOptions.views = Hoek.cloneWithShallow(serverOptions.views, 'engines'); var engines = Object.keys(serverOptions.views.engines); engines.forEach(function (engine) { var value = serverOptions.views.engines[engine]; if (typeof value === 'string') { if (options.relativeTo && value[0] === '.') { value = Path.join(options.relativeTo, value); } serverOptions.views.engines[engine] = require(value); } }); } pack.server(server.host, server.port, serverOptions); });
item.forEach(function (instance) { var registerOptions = Hoek.cloneWithShallow(instance, 'options'); delete registerOptions.options; plugins.push({ module: { plugin: require(path), options: instance.options }, apply: registerOptions }); });
exports.createServer = function () { var args = Pack._args(arguments); var settings = Hoek.cloneWithShallow(args.options || {}, ['app', 'plugins']); var options = { cache: settings.cache, debug: settings.debug }; delete settings.cache; var pack = new Pack(options); return pack.connection(args.host, args.port, settings); };
internals.parsePlugin = function (plugin, relativeTo) { plugin = Hoek.cloneWithShallow(plugin, ['options']); if (typeof plugin === 'string') { plugin = { register: plugin }; } let path = plugin.register; if (relativeTo && path[0] === '.') { path = Path.join(relativeTo, path); } plugin.register = require(path); return plugin; };
plugin.forEach(function (instance) { Hoek.assert(typeof instance === 'object', 'Invalid plugin configuration'); var registerOptions = Hoek.cloneWithShallow(instance, 'options'); delete registerOptions.options; plugins.push({ module: { register: require(path), options: instance.options }, apply: registerOptions }); });
const onResponse = (res) => { // Pass-through response const statusCode = res.statusCode; if (redirects === false || [301, 302, 307, 308].indexOf(statusCode) === -1) { return finishOnce(null, res); } // Redirection const redirectMethod = (statusCode === 301 || statusCode === 302 ? 'GET' : uri.method); let location = res.headers.location; res.destroy(); if (redirects === 0) { return finishOnce(Boom.badGateway('Maximum redirections reached', _trace)); } if (!location) { return finishOnce(Boom.badGateway('Received redirection without location', _trace)); } if (!/^https?:/i.test(location)) { location = Url.resolve(uri.href, location); } const redirectOptions = Hoek.cloneWithShallow(options, internals.shallowOptions); redirectOptions.payload = shadow || options.payload; // shadow must be ready at this point if set redirectOptions.redirects = --redirects; if (options.beforeRedirect) { options.beforeRedirect(redirectMethod, statusCode, location, redirectOptions); } const redirectReq = this.request(redirectMethod, location, redirectOptions, finishOnce, _trace); if (options.redirected) { options.redirected(statusCode, location, redirectReq); } };
const onResponse = (res) => { // Pass-through response const statusCode = res.statusCode; const redirectMethod = internals.redirectMethod(statusCode, uri.method, options); if (redirects === false || !redirectMethod) { return finishOnce(null, res); } // Redirection res.destroy(); if (redirects === 0) { return finishOnce(Boom.badGateway('Maximum redirections reached', _trace)); } let location = res.headers.location; if (!location) { return finishOnce(Boom.badGateway('Received redirection without location', _trace)); } if (!/^https?:/i.test(location)) { location = Url.resolve(uri.href, location); } const redirectOptions = Hoek.cloneWithShallow(options, internals.shallowOptions); redirectOptions.payload = shadow || options.payload; // shadow must be ready at this point if set redirectOptions.redirects = --redirects; return options.beforeRedirect(redirectMethod, statusCode, location, res.headers, redirectOptions, () => { const redirectReq = this._request(redirectMethod, location, redirectOptions, { callback: finishOnce }, _trace); if (options.redirected) { options.redirected(statusCode, location, redirectReq); } }); };
internals.implementation = function (server, options) { let settings = Hoek.cloneWithShallow(options, 'provider'); // Options can be reused // Lookup provider if (typeof settings.provider === 'object') { settings.name = settings.provider.name || 'custom'; } else { settings.name = settings.provider; settings.provider = Providers[settings.provider].call(null, settings.config); } const results = Joi.validate(settings, internals.schema); Hoek.assert(!results.error, results.error); // Passed validation, use Joi converted settings settings = results.value; // Setup cookie for managing temporary authorization state const cookieOptions = { encoding: 'iron', path: '/', password: settings.password, isSecure: settings.isSecure !== false, // Defaults to true isHttpOnly: settings.isHttpOnly !== false, // Defaults to true ttl: settings.ttl, domain: settings.domain, ignoreErrors: true, clearInvalid: true }; settings.cookie = settings.cookie || 'bell-' + settings.name; server.state(settings.cookie, cookieOptions); if (internals.simulate) { return internals.simulated(settings); } return { authenticate: (settings.provider.protocol === 'oauth' ? OAuth.v1 : OAuth.v2)(settings) }; };
internals.Client = function (options = {}) { Hoek.assert(!options.agents || (options.agents.https && options.agents.http && options.agents.httpsAllowUnauthorized), 'Option agents must include "http", "https", and "httpsAllowUnauthorized"'); this._defaults = Hoek.cloneWithShallow(options, internals.shallowOptions); this.agents = this._defaults.agents || { https: new Https.Agent({ maxSockets: Infinity }), http: new Http.Agent({ maxSockets: Infinity }), httpsAllowUnauthorized: new Https.Agent({ maxSockets: Infinity, rejectUnauthorized: false }) }; if (!options.events) { return; } this.events = new Events.EventEmitter(); this._emit = function (...args) { this.events.emit(...args); }; };
exports = module.exports = internals.Route = function (options, connection, plugin) { // Apply plugin environment (before schema validation) var realm = plugin.realm; if (realm.modifiers.route.vhost || realm.modifiers.route.prefix) { options = Hoek.cloneWithShallow(options, ['config']); // config is left unchanged options.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (options.path !== '/' ? options.path : '') : options.path); options.vhost = realm.modifiers.route.vhost || options.vhost; } // Setup and validate route configuration Hoek.assert(options.path, 'Route missing path'); Hoek.assert(options.handler || (options.config && options.config.handler), 'Missing or undefined handler:', options.method, options.path); Hoek.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must only appear once:', options.method, options.path); // XOR Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !connection.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when connection configured to strip:', options.method, options.path); Hoek.assert(/^[a-zA-Z0-9!#\$%&'\*\+\-\.^_`\|~]+$/.test(options.method), 'Invalid method name:', options.method, options.path); options = Schema.apply('route', options, options.path); var handler = options.handler || options.config.handler; var method = options.method.toLowerCase(); Hoek.assert(method !== 'head', 'Method name not allowed:', options.method, options.path); // Apply settings in order: {connection} <- {handler} <- {realm} <- {route} var handlerDefaults = Handler.defaults(method, handler, connection.server); var base = Hoek.applyToDefaultsWithShallow(connection.settings.routes, handlerDefaults, ['bind']); base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']); this.settings = Hoek.applyToDefaultsWithShallow(base, options.config || {}, ['bind']); this.settings.handler = handler; this.settings = Schema.apply('routeConfig', this.settings, options.path); var socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket); Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', options.path); Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', options.path); this.connection = connection; this.server = connection.server; this.path = options.path; this.method = method; this.plugin = plugin; this.public = { method: this.method, path: this.path, vhost: this.vhost, realm: this.plugin.realm, settings: this.settings }; this.settings.vhost = options.vhost; this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings // Path parsing this._analysis = this.connection._router.analyze(this.path); this.params = this._analysis.params; this.fingerprint = this._analysis.fingerprint; // Validation var validation = this.settings.validate; if (this.method === 'get') { // Assert on config, not on merged settings Hoek.assert(!options.config || !options.config.payload, 'Cannot set payload settings on HEAD or GET request:', options.path); Hoek.assert(!options.config || !options.config.validate || !options.config.validate.payload, 'Cannot validate HEAD or GET requests:', options.path); validation.payload = null; } ['headers', 'params', 'query', 'payload'].forEach(function (type) { validation[type] = internals.compileRule(validation[type]); }); if (this.settings.response.schema !== undefined || this.settings.response.status) { var rule = this.settings.response.schema; this.settings.response.status = this.settings.response.status || {}; var statuses = Object.keys(this.settings.response.status); if (rule === true && !statuses.length) { this.settings.response = null; } else { this.settings.response.schema = internals.compileRule(rule); for (var i = 0, il = statuses.length; i < il; ++i) { var code = statuses[i]; this.settings.response.status[code] = internals.compileRule(this.settings.response.status[code]); } } } else { this.settings.response = null; } // Payload parsing if (this.method === 'get') { this.settings.payload = null; } else { if (this.settings.payload.allow) { this.settings.payload.allow = [].concat(this.settings.payload.allow); } } Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', options.method, options.path); Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', options.path); // Authentication configuration this.settings.auth = this.connection.auth._setupRoute(this.settings.auth, options.path); // Cache if (this.method === 'get' && (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) { this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses); this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt }); } // CORS if (this.settings.cors) { this.settings.cors = Hoek.applyToDefaults(Defaults.cors, this.settings.cors); var cors = this.settings.cors; this.settings._cors = { headers: cors.headers.concat(cors.additionalHeaders).join(','), methods: cors.methods.concat(cors.additionalMethods).join(','), exposedHeaders: cors.exposedHeaders.concat(cors.additionalExposedHeaders).join(',') }; var _cors = this.settings._cors; if (cors.origin.length) { _cors.origin = { any: false, qualified: [], qualifiedString: '', wildcards: [] }; if (cors.origin.indexOf('*') !== -1) { Hoek.assert(cors.origin.length === 1, 'Cannot specify cors.origin * together with other values'); _cors.origin.any = true; } else { for (var c = 0, cl = cors.origin.length; c < cl; ++c) { var origin = cors.origin[c]; if (origin.indexOf('*') !== -1) { _cors.origin.wildcards.push(new RegExp('^' + Hoek.escapeRegex(origin).replace(/\\\*/g, '.*').replace(/\\\?/g, '.') + '$')); } else { _cors.origin.qualified.push(origin); } } Hoek.assert(cors.matchOrigin || !_cors.origin.wildcards.length, 'Cannot include wildcard origin values with matchOrigin disabled'); _cors.origin.qualifiedString = _cors.origin.qualified.join(' '); } } } // Security if (this.settings.security) { this.settings.security = Hoek.applyToDefaults(Defaults.security, this.settings.security); var security = this.settings.security; if (security.hsts) { if (security.hsts === true) { security._hsts = 'max-age=15768000'; } else if (typeof security.hsts === 'number') { security._hsts = 'max-age=' + security.hsts; } else { security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000); if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) { security._hsts += '; includeSubDomains'; } if (security.hsts.preload) { security._hsts += '; preload'; } } } if (security.xframe) { if (security.xframe === true) { security._xframe = 'DENY'; } else if (typeof security.xframe === 'string') { security._xframe = security.xframe.toUpperCase(); } else if (security.xframe.rule === 'allow-from') { if (!security.xframe.source) { security._xframe = 'SAMEORIGIN'; } else { security._xframe = 'ALLOW-FROM ' + security.xframe.source; } } else { security._xframe = security.xframe.rule.toUpperCase(); } } } // Handler this.settings.handler = Handler.configure(this.settings.handler, this); this._prerequisites = Handler.prerequisites(this.settings.pre, this.server); // Route lifecycle this._extensions = { onPreAuth: this._combineExtensions('onPreAuth'), onPostAuth: this._combineExtensions('onPostAuth'), onPreHandler: this._combineExtensions('onPreHandler'), onPostHandler: this._combineExtensions('onPostHandler'), onPreResponse: this._combineExtensions('onPreResponse') }; this._cycle = null; this.rebuild(); };
constructor(route, server, options = {}) { const core = server._core; const realm = server.realm; // Apply plugin environment (before schema validation) if (realm.modifiers.route.vhost || realm.modifiers.route.prefix) { route = Hoek.cloneWithShallow(route, ['options']); // options is left unchanged route.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path); route.vhost = realm.modifiers.route.vhost || route.vhost; } // Setup and validate route configuration Hoek.assert(route.path, 'Route missing path'); const routeDisplay = route.method + ' ' + route.path; let config = route.options || route.config; if (typeof config === 'function') { config = config.call(realm.settings.bind, server); } Hoek.assert(route.handler || (config && config.handler), 'Missing or undefined handler:', routeDisplay); Hoek.assert(!!route.handler ^ !!(config && config.handler), 'Handler must only appear once:', routeDisplay); // XOR Hoek.assert(route.path === '/' || route.path[route.path.length - 1] !== '/' || !core.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when configured to strip:', routeDisplay); config = Config.enable(config); route = Config.apply('route', route, routeDisplay); const handler = route.handler || config.handler; const method = route.method.toLowerCase(); Hoek.assert(method !== 'head', 'Method name not allowed:', routeDisplay); // Apply settings in order: {server} <- {handler} <- {realm} <- {route} const handlerDefaults = Handler.defaults(method, handler, core); let base = Hoek.applyToDefaultsWithShallow(core.settings.routes, handlerDefaults, ['bind']); base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']); this.settings = Hoek.applyToDefaultsWithShallow(base, config, ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query']); this.settings.handler = handler; this.settings = Config.apply('routeConfig', this.settings, routeDisplay); const socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket); Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', routeDisplay); Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', routeDisplay); this._core = core; this.path = route.path; this.method = method; this.realm = realm; this.settings.vhost = route.vhost; this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings // Path parsing this._special = !!options.special; this._analysis = this._core.router.analyze(this.path); this.params = this._analysis.params; this.fingerprint = this._analysis.fingerprint; this.public = { method: this.method, path: this.path, vhost: this.vhost, realm, settings: this.settings, fingerprint: this.fingerprint, auth: { access: (request) => Auth.testAccess(request, this.public) } }; // Validation const validation = this.settings.validate; if (this.method === 'get') { // Assert on config, not on merged settings Hoek.assert(!config.payload, 'Cannot set payload settings on HEAD or GET request:', routeDisplay); Hoek.assert(!config.validate || !config.validate.payload, 'Cannot validate HEAD or GET requests:', routeDisplay); validation.payload = null; } Hoek.assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters:', routeDisplay); ['headers', 'params', 'query', 'payload'].forEach((type) => { validation[type] = Validation.compile(validation[type]); }); if (this.settings.response.schema !== undefined || this.settings.response.status) { this.settings.response._validate = true; const rule = this.settings.response.schema; this.settings.response.status = this.settings.response.status || {}; const statuses = Object.keys(this.settings.response.status); if (rule === true && !statuses.length) { this.settings.response._validate = false; } else { this.settings.response.schema = Validation.compile(rule); for (let i = 0; i < statuses.length; ++i) { const code = statuses[i]; this.settings.response.status[code] = Validation.compile(this.settings.response.status[code]); } } } // Payload parsing if (this.method === 'get') { this.settings.payload = null; } else { this.settings.payload.decoders = this._core.compression._decoders; // Reference the shared object to keep up to date } Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', routeDisplay); Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', routeDisplay); // Authentication configuration this.settings.auth = (this._special ? false : this._core.auth._setupRoute(this.settings.auth, route.path)); // Cache if (this.method === 'get' && typeof this.settings.cache === 'object' && (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) { this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses); this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt }); } // CORS this.settings.cors = Cors.route(this.settings.cors); // Security this.settings.security = Security.route(this.settings.security); // Handler this.settings.handler = Handler.configure(this.settings.handler, this); this._prerequisites = Handler.prerequisitesConfig(this.settings.pre); // Route lifecycle this._extensions = { onPreResponse: Ext.combine(this, 'onPreResponse') }; if (this._special) { this._cycle = [internals.drain, Handler.execute]; this.rebuild(); return; } this._extensions.onPreAuth = Ext.combine(this, 'onPreAuth'); this._extensions.onCredentials = Ext.combine(this, 'onCredentials'); this._extensions.onPostAuth = Ext.combine(this, 'onPostAuth'); this._extensions.onPreHandler = Ext.combine(this, 'onPreHandler'); this._extensions.onPostHandler = Ext.combine(this, 'onPostHandler'); this.rebuild(); }
internals.Methods.prototype._add = function (name, fn, options, env) { var self = this; Hoek.assert(typeof fn === 'function', 'fn must be a function'); Hoek.assert(typeof name === 'string', 'name must be a string'); Hoek.assert(name.match(exports.methodNameRx), 'Invalid name:', name); Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists'); options = options || {}; Schema.assert('method', options, name); var settings = Hoek.cloneWithShallow(options, ['bind']); settings.generateKey = settings.generateKey || internals.generateKey; var bind = settings.bind || (env && env.bind) || null; // Create method settings.cache = settings.cache || {}; settings.cache.generateFunc = function (id, next) { id.args[id.args.length - 1] = next; // function (err, result, ttl) fn.apply(bind, id.args); }; var cache = (settings.cache.expiresIn || settings.cache.expiresAt ? this.pack._provisionCache(settings.cache, 'method', name, settings.cache.segment) : new Catbox.Policy(settings.cache)); var method = function (/* arguments, methodNext */) { var args = arguments; var methodNext = args[args.length - 1]; var key = settings.generateKey.apply(bind, args); if (key === null || typeof key !== 'string') { // Value can be '' self.pack.log(['hapi', 'method', 'key', 'error'], { name: name, args: args, key: key }); key = null; } cache.get({ id: key, args: args }, methodNext); }; method.cache = { drop: function (/* arguments, callback */) { var dropCallback = arguments[arguments.length - 1]; var key = settings.generateKey.apply(null, arguments); if (key === null) { // Value can be '' return Hoek.nextTick(dropCallback)(Boom.badImplementation('Invalid method key')); } return cache.drop(key, dropCallback); } }; // create method path var path = name.split('.'); var ref = this.methods; for (var i = 0, il = path.length; i < il; ++i) { if (!ref[path[i]]) { ref[path[i]] = (i + 1 === il ? method : {}); } ref = ref[path[i]]; } };
exports = module.exports = internals.Route = function (route, connection, plugin, options) { options = options || {}; // Apply plugin environment (before schema validation) const realm = plugin.realm; if (realm.modifiers.route.vhost || realm.modifiers.route.prefix) { route = Hoek.cloneWithShallow(route, ['config']); // config is left unchanged route.path = (realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path); route.vhost = realm.modifiers.route.vhost || route.vhost; } // Setup and validate route configuration Hoek.assert(route.path, 'Route missing path'); const routeDisplay = route.method + ' ' + route.path; Hoek.assert(route.handler || (route.config && route.config.handler), 'Missing or undefined handler:', routeDisplay); Hoek.assert(!!route.handler ^ !!(route.config && route.config.handler), 'Handler must only appear once:', routeDisplay); // XOR Hoek.assert(route.path === '/' || route.path[route.path.length - 1] !== '/' || !connection.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when connection configured to strip:', routeDisplay); route = Schema.apply('route', route, routeDisplay); const handler = route.handler || route.config.handler; const method = route.method.toLowerCase(); Hoek.assert(method !== 'head', 'Method name not allowed:', routeDisplay); // Apply settings in order: {connection} <- {handler} <- {realm} <- {route} const handlerDefaults = Handler.defaults(method, handler, connection.server); let base = Hoek.applyToDefaultsWithShallow(connection.settings.routes, handlerDefaults, ['bind']); base = Hoek.applyToDefaultsWithShallow(base, realm.settings, ['bind']); this.settings = Hoek.applyToDefaultsWithShallow(base, route.config || {}, ['bind']); this.settings.handler = handler; this.settings = Schema.apply('routeConfig', this.settings, routeDisplay); const socketTimeout = (this.settings.timeout.socket === undefined ? 2 * 60 * 1000 : this.settings.timeout.socket); Hoek.assert(!this.settings.timeout.server || !socketTimeout || this.settings.timeout.server < socketTimeout, 'Server timeout must be shorter than socket timeout:', routeDisplay); Hoek.assert(!this.settings.payload.timeout || !socketTimeout || this.settings.payload.timeout < socketTimeout, 'Payload timeout must be shorter than socket timeout:', routeDisplay); this.connection = connection; this.server = connection.server; this.path = route.path; this.method = method; this.plugin = plugin; this.settings.vhost = route.vhost; this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings // Path parsing this._special = !!options.special; this._analysis = this.connection._router.analyze(this.path); this.params = this._analysis.params; this.fingerprint = this._analysis.fingerprint; this.public = { method: this.method, path: this.path, vhost: this.vhost, realm: this.plugin.realm, settings: this.settings, fingerprint: this.fingerprint }; // Validation const validation = this.settings.validate; if (this.method === 'get') { // Assert on config, not on merged settings Hoek.assert(!route.config || !route.config.payload, 'Cannot set payload settings on HEAD or GET request:', routeDisplay); Hoek.assert(!route.config || !route.config.validate || !route.config.validate.payload, 'Cannot validate HEAD or GET requests:', routeDisplay); validation.payload = null; } ['headers', 'params', 'query', 'payload'].forEach((type) => { validation[type] = Validation.compile(validation[type]); }); if (this.settings.response.schema !== undefined || this.settings.response.status) { this.settings.response._validate = true; const rule = this.settings.response.schema; this.settings.response.status = this.settings.response.status || {}; const statuses = Object.keys(this.settings.response.status); if (rule === true && !statuses.length) { this.settings.response._validate = false; } else { this.settings.response.schema = Validation.compile(rule); for (let i = 0; i < statuses.length; ++i) { const code = statuses[i]; this.settings.response.status[code] = Validation.compile(this.settings.response.status[code]); } } } // Payload parsing if (this.method === 'get') { this.settings.payload = null; } else { if (this.settings.payload.allow) { this.settings.payload.allow = [].concat(this.settings.payload.allow); } } Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', routeDisplay); Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', routeDisplay); // Authentication configuration this.settings.auth = (this._special ? false : this.connection.auth._setupRoute(this.settings.auth, route.path)); // Cache if (this.method === 'get' && (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) { this.settings.cache._statuses = Hoek.mapToObject(this.settings.cache.statuses); this._cache = new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt }); } // CORS this.settings.cors = Cors.route(this.settings.cors); // Security if (this.settings.security) { this.settings.security = Hoek.applyToDefaults(Defaults.security, this.settings.security); const security = this.settings.security; if (security.hsts) { if (security.hsts === true) { security._hsts = 'max-age=15768000'; } else if (typeof security.hsts === 'number') { security._hsts = 'max-age=' + security.hsts; } else { security._hsts = 'max-age=' + (security.hsts.maxAge || 15768000); if (security.hsts.includeSubdomains || security.hsts.includeSubDomains) { security._hsts = security._hsts + '; includeSubDomains'; } if (security.hsts.preload) { security._hsts = security._hsts + '; preload'; } } } if (security.xframe) { if (security.xframe === true) { security._xframe = 'DENY'; } else if (typeof security.xframe === 'string') { security._xframe = security.xframe.toUpperCase(); } else if (security.xframe.rule === 'allow-from') { if (!security.xframe.source) { security._xframe = 'SAMEORIGIN'; } else { security._xframe = 'ALLOW-FROM ' + security.xframe.source; } } else { security._xframe = security.xframe.rule.toUpperCase(); } } } // Handler this.settings.handler = Handler.configure(this.settings.handler, this); this._prerequisites = Handler.prerequisitesConfig(this.settings.pre, this.server); // Route lifecycle this._extensions = { onPreResponse: this._combineExtensions('onPreResponse') }; if (this._special) { this._cycle = [Handler.execute]; return; } this._extensions.onPreAuth = this._combineExtensions('onPreAuth'); this._extensions.onPostAuth = this._combineExtensions('onPostAuth'); this._extensions.onPreHandler = this._combineExtensions('onPreHandler'); this._extensions.onPostHandler = this._combineExtensions('onPostHandler'); this.rebuild(); };
internals.Methods.prototype._add = function (name, method, options, realm) { var self = this; Hoek.assert(typeof method === 'function', 'method must be a function'); Hoek.assert(typeof name === 'string', 'name must be a string'); Hoek.assert(name.match(exports.methodNameRx), 'Invalid name:', name); Hoek.assert(!Hoek.reach(this.methods, name, { functions: false }), 'Server method function name already exists'); options = options || {}; Schema.assert('method', options, name); var settings = Hoek.cloneWithShallow(options, ['bind']); settings.generateKey = settings.generateKey || internals.generateKey; var bind = settings.bind || realm.settings.bind || null; var bound = bind ? function () { return method.apply(bind, arguments); } : method; // Normalize methods var normalized = bound; if (settings.callback === false) { // Defaults to true normalized = function (/* arg1, arg2, ..., argn, methodNext */) { var args = []; for (var i = 0, il = arguments.length; i < il - 1; ++i) { args.push(arguments[i]); } var methodNext = arguments[il - 1]; var result = null; var error = null; try { result = method.apply(bind, args); } catch (err) { error = err; } if (result instanceof Error) { error = result; result = null; } if (error || typeof result !== 'object' || typeof result.then !== 'function') { return methodNext(error, result); } // Promise object var onFulfilled = function (result) { return methodNext(null, result); }; var onRejected = function (err) { return methodNext(err); }; result.then(onFulfilled, onRejected); }; } // Not cached if (!settings.cache) { return this._assign(name, bound, normalized); } // Cached Hoek.assert(!settings.cache.generateFunc, 'Cannot set generateFunc with method caching'); settings.cache.generateFunc = function (id, next) { id.args.push(next); // function (err, result, ttl) normalized.apply(bind, id.args); }; var cache = this.server.cache(settings.cache, '#' + name); var func = function (/* arguments, methodNext */) { var args = []; for (var i = 0, il = arguments.length; i < il - 1; ++i) { args.push(arguments[i]); } var methodNext = arguments[il - 1]; var key = settings.generateKey.apply(bind, args); if (key === null || // Value can be '' typeof key !== 'string') { // When using custom generateKey return Hoek.nextTick(methodNext)(Boom.badImplementation('Invalid method key when invoking: ' + name, { name: name, args: args })); } cache.get({ id: key, args: args }, methodNext); }; func.cache = { drop: function (/* arguments, callback */) { var args = []; for (var i = 0, il = arguments.length; i < il - 1; ++i) { args.push(arguments[i]); } var methodNext = arguments[il - 1]; var key = settings.generateKey.apply(null, args); if (key === null) { // Value can be '' return Hoek.nextTick(methodNext)(Boom.badImplementation('Invalid method key')); } return cache.drop(key, methodNext); } }; this._assign(name, func, func); };
exports = module.exports = internals.Route = function (options, server, env) { // Apply plugin environment (before schema validation) if (env && (env.route.vhost || env.route.prefix)) { options = Hoek.cloneWithShallow(options, ['config']); options.path = (env.route.prefix ? env.route.prefix + (options.path !== '/' ? options.path : '') : options.path); options.vhost = env.route.vhost || options.vhost; } // Setup and validate route configuration Hoek.assert(options.handler || (options.config && options.config.handler), 'Missing or undefined handler:', options.path); Hoek.assert(!!options.handler ^ !!(options.config && options.config.handler), 'Handler must only appear once:', options.path); // XOR Hoek.assert(options.path === '/' || options.path[options.path.length - 1] !== '/' || !server.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when server configured to strip:', options.path); this.settings = Hoek.cloneWithShallow(options.config, ['bind', 'plugins', 'app']) || {}; this.settings.handler = this.settings.handler || options.handler; Schema.assert('route', options, options.path); Schema.assert('routeConfig', this.settings, options.path); this.server = server; this.path = options.path; this.method = options.method.toLowerCase(); this._env = env || {}; // Plugin-specific environment this.settings.method = this.method; // Expose method in settings this.settings.path = this.path; // Expose path in settings this.settings.vhost = options.vhost; // Expose vhost in settings this.settings.plugins = this.settings.plugins || {}; // Route-specific plugins settings, namespaced using plugin name this.settings.app = this.settings.app || {}; // Route-specific application settings // Path parsing this._analysis = this.server._router.analyze(this.path); this.params = this._analysis.params; this.fingerprint = this._analysis.fingerprint; // Validation this.settings.validate = this.settings.validate || {}; var validation = this.settings.validate; ['headers', 'params', 'query', 'payload'].forEach(function (type) { // null, undefined, true - anything allowed // false - nothing allowed // {...} - ... allowed var rule = validation[type]; validation[type] = (rule === false ? Joi.object({}) : typeof rule === 'function' ? rule : !rule || rule === true ? null // false tested earlier : Joi.compile(rule)); }); if (this.settings.response) { var rule = this.settings.response.schema; if (rule === true || this.settings.response.sample === 0) { this.settings.response = null; } else { this.settings.response.schema = (rule === false ? Joi.object({}) : typeof rule === 'function' ? rule : Joi.compile(rule)); } } // Payload parsing if (this.method !== 'get' && this.method !== 'head') { this.settings.payload = this.settings.payload || {}; var isProxy = (typeof this.settings.handler === 'object' && !!this.settings.handler.proxy); this.settings.payload.output = this.settings.payload.output || (isProxy ? 'stream' : 'data'); this.settings.payload.parse = this.settings.payload.parse !== undefined ? this.settings.payload.parse : !isProxy; this.settings.payload.maxBytes = this.settings.payload.maxBytes || this.server.settings.payload.maxBytes; this.settings.payload.uploads = this.settings.payload.uploads || this.server.settings.payload.uploads; this.settings.payload.failAction = this.settings.payload.failAction || 'error'; if (this.settings.payload.allow) { this.settings.payload.allow = [].concat(this.settings.payload.allow); } } else { Hoek.assert(!this.settings.payload, 'Cannot set payload settings on HEAD or GET request:', options.path); Hoek.assert(!this.settings.validate.payload, 'Cannot validate HEAD or GET requests:', options.path); } Hoek.assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled:', options.path); Hoek.assert(!this.settings.jsonp || typeof this.settings.jsonp === 'string', 'Bad route JSONP parameter name:', options.path); // Authentication configuration this.settings.auth = this.server.auth._setupRoute(this.settings.auth, options.path); // Cache Hoek.assert(!this.settings.cache || this.method === 'get', 'Only GET routes can use a cache:', options.path); this._cache = this.settings.cache ? new Catbox.Policy({ expiresIn: this.settings.cache.expiresIn, expiresAt: this.settings.cache.expiresAt }) : null; // Handler this.settings.handler = Handler.configure(this.settings.handler, this); this._prerequisites = Handler.prerequisites(this.settings.pre, server); // Route lifecycle this._cycle = this.lifecycle(); };