UNPKG

@hapi/hapi

Version:

HTTP Server framework

490 lines (343 loc) 14.8 kB
'use strict'; const Assert = require('assert'); const Bounce = require('@hapi/bounce'); const Catbox = require('@hapi/catbox'); const Hoek = require('@hapi/hoek'); const Subtext = require('@hapi/subtext'); const Validate = require('@hapi/validate'); const Auth = require('./auth'); const Config = require('./config'); const Cors = require('./cors'); const Ext = require('./ext'); const Handler = require('./handler'); const Headers = require('./headers'); const Security = require('./security'); const Streams = require('./streams'); const Validation = require('./validation'); const internals = {}; exports = module.exports = internals.Route = class { constructor(route, server, options = {}) { const core = server._core; const realm = server.realm; // Routing information Config.apply('route', route, route.method, route.path); const method = route.method.toLowerCase(); Hoek.assert(method !== 'head', 'Cannot set HEAD route:', route.path); const path = realm.modifiers.route.prefix ? realm.modifiers.route.prefix + (route.path !== '/' ? route.path : '') : route.path; Hoek.assert(path === '/' || path[path.length - 1] !== '/' || !core.settings.router.stripTrailingSlash, 'Path cannot end with a trailing slash when configured to strip:', route.method, route.path); const vhost = realm.modifiers.route.vhost ?? route.vhost; // Set identifying members (assert) this.method = method; this.path = path; // Prepare configuration let config = route.options ?? route.config ?? {}; if (typeof config === 'function') { config = config.call(realm.settings.bind, server); } config = Config.enable(config); // Shallow clone // Verify route level config (as opposed to the merged settings) this._assert(method !== 'get' || !config.payload, 'Cannot set payload settings on HEAD or GET request'); this._assert(method !== 'get' || !config.validate?.payload, 'Cannot validate HEAD or GET request payload'); // Rules this._assert(!route.rules || !config.rules, 'Route rules can only appear once'); // XOR const rules = route.rules ?? config.rules; const rulesConfig = internals.rules(rules, { method, path, vhost }, server); delete config.rules; // Handler this._assert(route.handler || config.handler, 'Missing or undefined handler'); this._assert(!!route.handler ^ !!config.handler, 'Handler must only appear once'); // XOR const handler = Config.apply('handler', route.handler ?? config.handler); delete config.handler; const handlerDefaults = Handler.defaults(method, handler, core); // Apply settings in order: server <- handler <- realm <- route const settings = internals.config([core.settings.routes, handlerDefaults, realm.settings, rulesConfig, config]); this.settings = Config.apply('routeConfig', settings, method, path); // Route members this._core = core; this.realm = realm; this.settings.vhost = 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, realm, settings: this.settings, fingerprint: this.fingerprint, auth: { access: (request) => Auth.testAccess(request, this.public) } }; // Validation this._setupValidation(); // 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 } this._assert(!this.settings.validate.payload || this.settings.payload.parse, 'Route payload must be set to \'parse\' when payload validation enabled'); this._assert(!this.settings.validate.state || this.settings.state.parse, 'Route state must be set to \'parse\' when state validation enabled'); // Authentication configuration this.settings.auth = this._special ? false : this._core.auth._setupRoute(this.settings.auth, path); // Cache if (this.method === 'get' && typeof this.settings.cache === 'object' && (this.settings.cache.expiresIn || this.settings.cache.expiresAt)) { this.settings.cache._statuses = new Set(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(handler, this); this._prerequisites = Handler.prerequisitesConfig(this.settings.pre); // Route lifecycle this._extensions = { onPreResponse: Ext.combine(this, 'onPreResponse'), onPostResponse: Ext.combine(this, 'onPostResponse') }; 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(); } _setupValidation() { const validation = this.settings.validate; if (this.method === 'get') { validation.payload = null; } this._assert(!validation.params || this.params.length, 'Cannot set path parameters validations without path parameters'); for (const type of ['headers', 'params', 'query', 'payload', 'state']) { validation[type] = Validation.compile(validation[type], this.settings.validate.validator, this.realm, this._core); } 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, this.settings.validate.validator, this.realm, this._core); for (const code of statuses) { this.settings.response.status[code] = Validation.compile(this.settings.response.status[code], this.settings.validate.validator, this.realm, this._core); } } } } rebuild(event) { if (event) { this._extensions[event.type].add(event); } if (this._special) { this._postCycle = this._extensions.onPreResponse.nodes ? [this._extensions.onPreResponse] : []; this._buildMarshalCycle(); return; } // Build lifecycle array this._cycle = []; // 'onRequest' if (this.settings.state.parse) { this._cycle.push(internals.state); } if (this._extensions.onPreAuth.nodes) { this._cycle.push(this._extensions.onPreAuth); } if (this._core.auth._enabled(this, 'authenticate')) { this._cycle.push(Auth.authenticate); } if (this.method !== 'get') { this._cycle.push(internals.payload); if (this._core.auth._enabled(this, 'payload')) { this._cycle.push(Auth.payload); } } if (this._core.auth._enabled(this, 'authenticate') && this._extensions.onCredentials.nodes) { this._cycle.push(this._extensions.onCredentials); } if (this._core.auth._enabled(this, 'access')) { this._cycle.push(Auth.access); } if (this._extensions.onPostAuth.nodes) { this._cycle.push(this._extensions.onPostAuth); } if (this.settings.validate.headers) { this._cycle.push(Validation.headers); } if (this.settings.validate.params) { this._cycle.push(Validation.params); } if (this.settings.validate.query) { this._cycle.push(Validation.query); } if (this.settings.validate.payload) { this._cycle.push(Validation.payload); } if (this.settings.validate.state) { this._cycle.push(Validation.state); } if (this._extensions.onPreHandler.nodes) { this._cycle.push(this._extensions.onPreHandler); } this._cycle.push(Handler.execute); if (this._extensions.onPostHandler.nodes) { this._cycle.push(this._extensions.onPostHandler); } this._postCycle = []; if (this.settings.response._validate && this.settings.response.sample !== 0) { this._postCycle.push(Validation.response); } if (this._extensions.onPreResponse.nodes) { this._postCycle.push(this._extensions.onPreResponse); } this._buildMarshalCycle(); // onPostResponse } _buildMarshalCycle() { this._marshalCycle = [Headers.type]; if (this.settings.cors) { this._marshalCycle.push(Cors.headers); } if (this.settings.security) { this._marshalCycle.push(Security.headers); } this._marshalCycle.push(Headers.entity); if (this.method === 'get' || this.method === '*') { this._marshalCycle.push(Headers.unmodified); } this._marshalCycle.push(Headers.cache); this._marshalCycle.push(Headers.state); this._marshalCycle.push(Headers.content); if (this._core.auth._enabled(this, 'response')) { this._marshalCycle.push(Auth.response); // Must be last in case requires access to headers } } _assert(condition, message) { if (condition) { return; } if (this.method[0] !== '_') { message = `${message}: ${this.method.toUpperCase()} ${this.path}`; } throw new Assert.AssertionError({ message, actual: false, expected: true, operator: '==', stackStartFunction: this._assert }); } }; internals.state = async function (request) { request.state = {}; const req = request.raw.req; const cookies = req.headers.cookie; if (!cookies) { return; } try { var result = await request._core.states.parse(cookies); } catch (err) { Bounce.rethrow(err, 'system'); var parseError = err; } const { states, failed = [] } = result ?? parseError; request.state = states ?? {}; // Clear cookies for (const item of failed) { if (item.settings.clearInvalid) { request._clearState(item.name); } } if (!parseError) { return; } parseError.header = cookies; return request._core.toolkit.failAction(request, request.route.settings.state.failAction, parseError, { tags: ['state', 'error'] }); }; internals.payload = async function (request) { if (request.method === 'get' || request.method === 'head') { // When route.method is '*' return; } if (request.payload !== undefined) { return internals.drain(request); } if (request._expectContinue) { request._expectContinue = false; request.raw.res.writeContinue(); } try { const { payload, mime } = await Subtext.parse(request.raw.req, request._tap(), request.route.settings.payload); request._isPayloadPending = !!payload?._readableState; request.mime = mime; request.payload = payload; } catch (err) { Bounce.rethrow(err, 'system'); await internals.drain(request); request.mime = err.mime; request.payload = null; return request._core.toolkit.failAction(request, request.route.settings.payload.failAction, err, { tags: ['payload', 'error'] }); } }; internals.drain = async function (request) { // Flush out any pending request payload not consumed due to errors if (request._expectContinue) { request._isPayloadPending = false; // If we don't continue, client should not send a payload request._expectContinue = false; } if (request._isPayloadPending) { await Streams.drain(request.raw.req); request._isPayloadPending = false; } }; internals.config = function (chain) { if (!chain.length) { return {}; } let config = chain[0]; for (const item of chain) { config = Hoek.applyToDefaults(config, item, { shallow: ['bind', 'validate.headers', 'validate.payload', 'validate.params', 'validate.query', 'validate.state'] }); } return config; }; internals.rules = function (rules, info, server) { const configs = []; let realm = server.realm; while (realm) { if (realm._rules) { const source = !realm._rules.settings.validate ? rules : Validate.attempt(rules, realm._rules.settings.validate.schema, realm._rules.settings.validate.options); const config = realm._rules.processor(source, info); if (config) { configs.unshift(config); } } realm = realm.parent; } return internals.config(configs); };