UNPKG

fastify

Version:

Fast and low overhead web framework, for Node.js

351 lines (321 loc) 9.36 kB
'use strict' const proxyAddr = require('@fastify/proxy-addr') const { kHasBeenDecorated, kSchemaBody, kSchemaHeaders, kSchemaParams, kSchemaQuerystring, kSchemaController, kOptions, kRequestCacheValidateFns, kRouteContext, kRequestOriginalUrl } = require('./symbols') const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION } = require('./errors') const HTTP_PART_SYMBOL_MAP = { body: kSchemaBody, headers: kSchemaHeaders, params: kSchemaParams, querystring: kSchemaQuerystring, query: kSchemaQuerystring } function Request (id, params, req, query, log, context) { this.id = id this[kRouteContext] = context this.params = params this.raw = req this.query = query this.log = log this.body = undefined } Request.props = [] function getTrustProxyFn (tp) { if (typeof tp === 'function') { return tp } if (tp === true) { // Support trusting everything return null } if (typeof tp === 'number') { // Support trusting hop count return function (a, i) { return i < tp } } if (typeof tp === 'string') { // Support comma-separated tps const values = tp.split(',').map(it => it.trim()) return proxyAddr.compile(values) } return proxyAddr.compile(tp) } function buildRequest (R, trustProxy) { if (trustProxy) { return buildRequestWithTrustProxy(R, trustProxy) } return buildRegularRequest(R) } function buildRegularRequest (R) { const props = R.props.slice() function _Request (id, params, req, query, log, context) { this.id = id this[kRouteContext] = context this.params = params this.raw = req this.query = query this.log = log this.body = undefined let prop for (let i = 0; i < props.length; i++) { prop = props[i] this[prop.key] = prop.value } } Object.setPrototypeOf(_Request.prototype, R.prototype) Object.setPrototypeOf(_Request, R) _Request.props = props _Request.parent = R return _Request } function getLastEntryInMultiHeaderValue (headerValue) { // we use the last one if the header is set more than once const lastIndex = headerValue.lastIndexOf(',') return lastIndex === -1 ? headerValue.trim() : headerValue.slice(lastIndex + 1).trim() } function buildRequestWithTrustProxy (R, trustProxy) { const _Request = buildRegularRequest(R) const proxyFn = getTrustProxyFn(trustProxy) // This is a more optimized version of decoration _Request[kHasBeenDecorated] = true Object.defineProperties(_Request.prototype, { ip: { get () { const addrs = proxyAddr.all(this.raw, proxyFn) return addrs[addrs.length - 1] } }, ips: { get () { return proxyAddr.all(this.raw, proxyFn) } }, host: { get () { if (this.ip !== undefined && this.headers['x-forwarded-host']) { return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-host']) } /** * The last fallback supports the following cases: * 1. http.requireHostHeader === false * 2. HTTP/1.0 without a Host Header * 3. Headers schema that may remove the Host Header */ return this.headers.host ?? this.headers[':authority'] ?? '' } }, protocol: { get () { if (this.headers['x-forwarded-proto']) { return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto']) } if (this.socket) { return this.socket.encrypted ? 'https' : 'http' } } } }) return _Request } Object.defineProperties(Request.prototype, { server: { get () { return this[kRouteContext].server } }, url: { get () { return this.raw.url } }, originalUrl: { get () { /* istanbul ignore else */ if (!this[kRequestOriginalUrl]) { this[kRequestOriginalUrl] = this.raw.originalUrl || this.raw.url } return this[kRequestOriginalUrl] } }, method: { get () { return this.raw.method } }, routeOptions: { get () { const context = this[kRouteContext] const routeLimit = context._parserOptions.limit const serverLimit = context.server.initialConfig.bodyLimit const version = context.server.hasConstraintStrategy('version') ? this.raw.headers['accept-version'] : undefined const options = { method: context.config?.method, url: context.config?.url, bodyLimit: (routeLimit || serverLimit), attachValidation: context.attachValidation, logLevel: context.logLevel, exposeHeadRoute: context.exposeHeadRoute, prefixTrailingSlash: context.prefixTrailingSlash, handler: context.handler, version } Object.defineProperties(options, { config: { get: () => context.config }, schema: { get: () => context.schema } }) return Object.freeze(options) } }, is404: { get () { return this[kRouteContext].config?.url === undefined } }, socket: { get () { return this.raw.socket } }, ip: { get () { if (this.socket) { return this.socket.remoteAddress } } }, host: { get () { /** * The last fallback supports the following cases: * 1. http.requireHostHeader === false * 2. HTTP/1.0 without a Host Header * 3. Headers schema that may remove the Host Header */ return this.raw.headers.host ?? this.raw.headers[':authority'] ?? '' } }, hostname: { get () { return this.host.split(':', 1)[0] } }, port: { get () { // first try taking port from host const portFromHost = parseInt(this.host.split(':').slice(-1)[0]) if (!isNaN(portFromHost)) { return portFromHost } // now fall back to port from host/:authority header const host = (this.headers.host ?? this.headers[':authority'] ?? '') const portFromHeader = parseInt(host.split(':').slice(-1)[0]) if (!isNaN(portFromHeader)) { return portFromHeader } // fall back to null return null } }, protocol: { get () { if (this.socket) { return this.socket.encrypted ? 'https' : 'http' } } }, headers: { get () { if (this.additionalHeaders) { return Object.assign({}, this.raw.headers, this.additionalHeaders) } return this.raw.headers }, set (headers) { this.additionalHeaders = headers } }, getValidationFunction: { value: function (httpPartOrSchema) { if (typeof httpPartOrSchema === 'string') { const symbol = HTTP_PART_SYMBOL_MAP[httpPartOrSchema] return this[kRouteContext][symbol] } else if (typeof httpPartOrSchema === 'object') { return this[kRouteContext][kRequestCacheValidateFns]?.get(httpPartOrSchema) } } }, compileValidationSchema: { value: function (schema, httpPart = null) { const { method, url } = this if (this[kRouteContext][kRequestCacheValidateFns]?.has(schema)) { return this[kRouteContext][kRequestCacheValidateFns].get(schema) } const validatorCompiler = this[kRouteContext].validatorCompiler || this.server[kSchemaController].validatorCompiler || ( // We compile the schemas if no custom validatorCompiler is provided // nor set this.server[kSchemaController].setupValidator(this.server[kOptions]) || this.server[kSchemaController].validatorCompiler ) const validateFn = validatorCompiler({ schema, method, url, httpPart }) // We create a WeakMap to compile the schema only once // Its done lazily to avoid add overhead by creating the WeakMap // if it is not used // TODO: Explore a central cache for all the schemas shared across // encapsulated contexts if (this[kRouteContext][kRequestCacheValidateFns] == null) { this[kRouteContext][kRequestCacheValidateFns] = new WeakMap() } this[kRouteContext][kRequestCacheValidateFns].set(schema, validateFn) return validateFn } }, validateInput: { value: function (input, schema, httpPart) { httpPart = typeof schema === 'string' ? schema : httpPart const symbol = (httpPart != null && typeof httpPart === 'string') && HTTP_PART_SYMBOL_MAP[httpPart] let validate if (symbol) { // Validate using the HTTP Request Part schema validate = this[kRouteContext][symbol] } // We cannot compile if the schema is missed if (validate == null && (schema == null || typeof schema !== 'object' || Array.isArray(schema)) ) { throw new FST_ERR_REQ_INVALID_VALIDATION_INVOCATION(httpPart) } if (validate == null) { if (this[kRouteContext][kRequestCacheValidateFns]?.has(schema)) { validate = this[kRouteContext][kRequestCacheValidateFns].get(schema) } else { // We proceed to compile if there's no validate function yet validate = this.compileValidationSchema(schema, httpPart) } } return validate(input) } } }) module.exports = Request module.exports.buildRequest = buildRequest