UNPKG

hapi

Version:

HTTP Server framework

624 lines (444 loc) 18.3 kB
'use strict'; const Url = require('url'); const Boom = require('boom'); const Bounce = require('bounce'); const Hoek = require('hoek'); const Podium = require('podium'); const Cors = require('./cors'); const Response = require('./response'); const Transmit = require('./transmit'); const internals = { events: Podium.validate(['finish', { name: 'peek', spread: true }, 'disconnect']), reserved: ['server', 'url', 'query', 'path', 'method', 'mime', 'setUrl', 'setMethod', 'headers', 'id', 'app', 'plugins', 'route', 'auth', 'pre', 'preResponses', 'info', 'orig', 'params', 'paramsArray', 'payload', 'state', 'jsonp', 'response', 'raw', 'domain', 'log', 'logs', 'generateResponse'] }; exports = module.exports = internals.Request = class { constructor(server, req, res, options) { this._allowInternals = !!options.allowInternals; this._core = server._core; this._entity = null; // Entity information set via h.entity() this._eventContext = { request: this }; this._events = null; // Assigned an emitter when request.events is accessed this._expectContinue = !!options.expectContinue; this._isPayloadPending = !!(req.headers['content-length'] || req.headers['transfer-encoding']); // Changes to false when incoming payload fully processed this._isReplied = false; // true when response processing started this._route = this._core.router.specials.notFound.route; // Used prior to routing (only settings are used, not the handler) this._serverTimeoutId = null; this._states = {}; this._urlError = null; this.app = (options.app ? Object.assign({}, options.app) : {}); // Place for application-specific state without conflicts with hapi, should not be used by plugins (shallow cloned) this.headers = req.headers; this.info = internals.info(this._core, req); this.jsonp = null; this.logs = []; this.method = req.method.toLowerCase(); this.mime = null; this.orig = {}; this.params = null; this.paramsArray = null; // Array of path parameters in path order this.path = null; this.payload = null; this.plugins = (options.plugins ? Object.assign({}, options.plugins) : {}); // Place for plugins to store state without conflicts with hapi, should be namespaced using plugin name (shallow cloned) this.pre = {}; // Pre raw values this.preResponses = {}; // Pre response values this.raw = { req, res }; this.response = null; this.route = this._route.public; this.query = null; this.server = server; this.state = null; this.url = null; this.auth = { isAuthenticated: false, isAuthorized: false, credentials: options.auth ? options.auth.credentials : null, // Special keys: 'app', 'user', 'scope' artifacts: options.auth && options.auth.artifacts || null, // Scheme-specific artifacts strategy: options.auth ? options.auth.strategy : null, mode: null, error: null }; if (options.auth) { this.auth.isInjected = true; } // Parse request url this._initializeUrl(); } static generate(server, req, res, options) { const request = new server._core.Request(server, req, res, options); // Decorate if (server._core._decorations.requestApply) { for (const property in server._core._decorations.requestApply) { const assignment = server._core._decorations.requestApply[property]; request[property] = assignment(request); } } request._listen(); return request; } get events() { if (!this._events) { this._events = new Podium(internals.events); } return this._events; } _initializeUrl() { try { this._setUrl(this.raw.req.url, this._core.settings.router.stripTrailingSlash); } catch (err) { this.path = this.raw.req.url; this.query = {}; this._urlError = Boom.boomify(err, { statusCode: 400, override: false }); } } setUrl(url, stripTrailingSlash) { Hoek.assert(this.params === null, 'Cannot change request URL after routing'); if (url instanceof Url.URL) { url = url.href; } Hoek.assert(typeof url === 'string', 'Url must be a string or URL object'); this._setUrl(url, stripTrailingSlash); this._urlError = null; } _setUrl(url, stripTrailingSlash) { const base = (url[0] === '/' ? `${this._core.info.protocol}://${this.info.host || `${this._core.info.host}:${this._core.info.port}`}` : undefined); url = new Url.URL(url, base); // Apply path modifications let path = this._core.router.normalize(url.pathname); // pathname excludes query if (stripTrailingSlash && path.length > 1 && path[path.length - 1] === '/') { path = path.slice(0, -1); } url.pathname = path; // Parse query (must be done before this.url is set in case query parsing throws) this.query = this._parseQuery(url.searchParams); // Store request properties this.url = url; this.path = path; this.info.hostname = url.hostname; this.info.host = url.host; } _parseQuery(searchParams) { // Flatten map let query = Object.create(null); for (let [key, value] of searchParams) { const entry = query[key]; if (entry !== undefined) { value = [].concat(entry, value); } query[key] = value; } // Custom parser const parser = this._core.settings.query.parser; if (parser) { query = parser(query); if (!query || typeof query !== 'object') { throw Boom.badImplementation('Parsed query must be an object'); } } return query; } setMethod(method) { Hoek.assert(this.params === null, 'Cannot change request method after routing'); Hoek.assert(method && typeof method === 'string', 'Missing method'); this.method = method.toLowerCase(); } active() { return !!this._eventContext.request; } async _execute() { this.info.acceptEncoding = this._core.compression.accept(this); try { await this._onRequest(); } catch (err) { Bounce.rethrow(err, 'system'); return this._reply(err); } this._lookup(); this._setTimeouts(); await this._lifecycle(); this._reply(); } async _onRequest() { // onRequest (can change request method and url) if (this._core.extensions.route.onRequest.nodes) { const response = await this._invoke(this._core.extensions.route.onRequest); if (response) { if (!internals.skip(response)) { throw Boom.badImplementation('onRequest extension methods must return an error, a takeover response, or a continue signal'); } throw response; } } // Validate path if (this._urlError) { throw this._urlError; } } _listen() { if (this._isPayloadPending) { this.raw.req.on('end', internals.event.bind(this.raw.req, this._eventContext, 'end')); } this.raw.req.on('close', internals.event.bind(this.raw.req, this._eventContext, 'close')); this.raw.req.on('error', internals.event.bind(this.raw.req, this._eventContext, 'error')); this.raw.req.on('aborted', internals.event.bind(this.raw.req, this._eventContext, 'abort')); } _lookup() { const match = this._core.router.route(this.method, this.path, this.info.hostname); if (!match.route.settings.isInternal || this._allowInternals) { this._route = match.route; this.route = this._route.public; } this.params = match.params || {}; this.paramsArray = match.paramsArray || []; if (this.route.settings.cors) { this.info.cors = { isOriginMatch: Cors.matchOrigin(this.headers.origin, this.route.settings.cors) }; } } _setTimeouts() { if (this.raw.req.socket && this.route.settings.timeout.socket !== undefined) { this.raw.req.socket.setTimeout(this.route.settings.timeout.socket || 0); // Value can be false or positive } let serverTimeout = this.route.settings.timeout.server; if (!serverTimeout) { return; } const elapsed = Date.now() - this.info.received; serverTimeout = Math.floor(serverTimeout - elapsed); // Calculate the timeout from when the request was constructed if (serverTimeout <= 0) { internals.timeoutReply(this, serverTimeout); return; } this._serverTimeoutId = setTimeout(internals.timeoutReply, serverTimeout, this, serverTimeout); } async _lifecycle() { for (const func of this._route._cycle) { if (this._isReplied || !this._eventContext.request) { return; } try { var response = await (typeof func === 'function' ? func(this) : this._invoke(func)); } catch (err) { Bounce.rethrow(err, 'system'); response = Response.wrap(err, this); } if (!response || response === this._core.toolkit.continue) { // Continue continue; } if (!internals.skip(response)) { response = Boom.badImplementation('Lifecycle methods called before the handler can only return an error, a takeover response, or a continue signal'); } this._setResponse(response); return; } } async _invoke(event) { for (const ext of event.nodes) { const bind = (ext.bind || ext.realm.settings.bind); const realm = ext.realm; const response = await this._core.toolkit.execute(ext.func, this, { bind, realm }); if (response === this._core.toolkit.continue) { continue; } if (internals.skip(response) || this.response === null) { return response; } this._setResponse(response); } } async _reply(exit) { if (this._isReplied) { // Prevent any future responses to this request return; } this._isReplied = true; if (this._serverTimeoutId) { clearTimeout(this._serverTimeoutId); } if (!this._eventContext.request) { this._finalize(); return; } if (exit) { // Can be a valid response or error (if returned from an ext, already handled because this.response is also set) this._setResponse(Response.wrap(exit, this)); // Wrap to ensure any object thrown is always a valid Boom or Response object } if (typeof this.response === 'symbol') { // close or abandon this._abort(); return; } await this._postCycle(); if (!this._eventContext.request || typeof this.response === 'symbol') { // close or abandon this._abort(); return; } await Transmit.send(this); this._finalize(); } async _postCycle() { for (const func of this._route._postCycle) { if (!this._eventContext.request) { return; } try { var response = await (typeof func === 'function' ? func(this) : this._invoke(func)); } catch (err) { Bounce.rethrow(err, 'system'); response = Response.wrap(err, this); } if (response && response !== this._core.toolkit.continue) { // Continue this._setResponse(response); } } } _abort() { if (this.response === this._core.toolkit.close) { this.raw.res.end(); // End the response in case it wasn't already closed } this._finalize(); } _finalize() { if (this.response && this.response.statusCode === 500 && this.response._error) { const tags = this.response._error.isDeveloperError ? ['internal', 'implementation', 'error'] : ['internal', 'error']; this._log(tags, this.response._error, 'error'); } // Cleanup this._eventContext.request = null; // Disable req events if (this.response && this.response._close) { this.response._close(this); } this.info.completed = Date.now(); this._core.events.emit('response', this); this._core.queue.release(); } _setResponse(response) { if (this.response && !this.response.isBoom && this.response !== response && (response.isBoom || this.response.source !== response.source)) { this.response._close(this); } if (this.info.completed) { if (response._close) { response._close(this); } return; } this.response = response; } _setState(name, value, options) { const state = { name, value }; if (options) { Hoek.assert(!options.autoValue, 'Cannot set autoValue directly in a response'); state.options = Hoek.clone(options); } this._states[name] = state; } _clearState(name, options = {}) { const state = { name }; state.options = Hoek.clone(options); state.options.ttl = 0; this._states[name] = state; } _tap() { if (!this._events) { return null; } return (this._events.hasListeners('finish') || this._events.hasListeners('peek') ? new Response.Peek(this._events) : null); } log(tags, data) { return this._log(tags, data, 'app'); } _log(tags, data, channel = 'internal') { if (!this._core.events.hasListeners('request') && !this.route.settings.log.collect) { return; } if (!Array.isArray(tags)) { tags = [tags]; } const timestamp = Date.now(); const field = (data instanceof Error ? 'error' : 'data'); let event = [this, { request: this.info.id, timestamp, tags, [field]: data, channel }]; if (typeof data === 'function') { event = () => [this, { request: this.info.id, timestamp, tags, data: data(), channel }]; } if (this.route.settings.log.collect) { if (typeof data === 'function') { event = event(); } this.logs.push(event[1]); } this._core.events.emit({ name: 'request', channel, tags }, event); } generateResponse(source, options) { return new Response(source, this, options); } }; internals.Request.reserved = internals.reserved; internals.info = function (core, req) { const host = req.headers.host ? req.headers.host.trim() : ''; const received = Date.now(); const info = { received, remoteAddress: req.connection.remoteAddress, remotePort: req.connection.remotePort || '', referrer: req.headers.referrer || req.headers.referer || '', host, hostname: host.split(':')[0], id: `${received}:${core.info.id}:${core.requestCounter.value++}`, // Assigned later acceptEncoding: null, cors: null, responded: 0, completed: 0 }; if (core.requestCounter.value > core.requestCounter.max) { core.requestCounter.value = core.requestCounter.min; } return info; }; internals.event = function ({ request }, event, err) { if (!request) { return; } request._isPayloadPending = false; if (event === 'close' && request.raw.res.finished) { return; } if (event === 'end') { return; } request._log(err ? ['request', 'error'] : ['request', 'error', event], err); if (event === 'error') { return; } request._eventContext.request = null; if (event === 'abort' && request._events) { request._events.emit('disconnect'); } }; internals.timeoutReply = function (request, timeout) { const elapsed = Date.now() - request.info.received; request._log(['request', 'server', 'timeout', 'error'], { timeout, elapsed }); request._reply(Boom.serverUnavailable()); }; internals.skip = function (response) { return (response.isBoom || response._takeover || typeof response === 'symbol'); };