strong-tie/inbound-calls
0
1'use strict'2 3const proxyAddr = require('@fastify/proxy-addr')4const {5 kHasBeenDecorated,6 kSchemaBody,7 kSchemaHeaders,8 kSchemaParams,9 kSchemaQuerystring,10 kSchemaController,11 kOptions,12 kRequestCacheValidateFns,13 kRouteContext,14 kRequestOriginalUrl15} = require('./symbols')16const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION } = require('./errors')17 18const HTTP_PART_SYMBOL_MAP = {19 body: kSchemaBody,20 headers: kSchemaHeaders,21 params: kSchemaParams,22 querystring: kSchemaQuerystring,23 query: kSchemaQuerystring24}25 26function Request (id, params, req, query, log, context) {27 this.id = id28 this[kRouteContext] = context29 this.params = params30 this.raw = req31 this.query = query32 this.log = log33 this.body = undefined34}35Request.props = []36 37function getTrustProxyFn (tp) {38 if (typeof tp === 'function') {39 return tp40 }41 if (tp === true) {42 // Support trusting everything43 return null44 }45 if (typeof tp === 'number') {46 // Support trusting hop count47 return function (a, i) { return i < tp }48 }49 if (typeof tp === 'string') {50 // Support comma-separated tps51 const values = tp.split(',').map(it => it.trim())52 return proxyAddr.compile(values)53 }54 return proxyAddr.compile(tp)55}56 57function buildRequest (R, trustProxy) {58 if (trustProxy) {59 return buildRequestWithTrustProxy(R, trustProxy)60 }61 62 return buildRegularRequest(R)63}64 65function buildRegularRequest (R) {66 const props = R.props.slice()67 function _Request (id, params, req, query, log, context) {68 this.id = id69 this[kRouteContext] = context70 this.params = params71 this.raw = req72 this.query = query73 this.log = log74 this.body = undefined75 76 let prop77 for (let i = 0; i < props.length; i++) {78 prop = props[i]79 this[prop.key] = prop.value80 }81 }82 Object.setPrototypeOf(_Request.prototype, R.prototype)83 Object.setPrototypeOf(_Request, R)84 _Request.props = props85 _Request.parent = R86 87 return _Request88}89 90function getLastEntryInMultiHeaderValue (headerValue) {91 // we use the last one if the header is set more than once92 const lastIndex = headerValue.lastIndexOf(',')93 return lastIndex === -1 ? headerValue.trim() : headerValue.slice(lastIndex + 1).trim()94}95 96function buildRequestWithTrustProxy (R, trustProxy) {97 const _Request = buildRegularRequest(R)98 const proxyFn = getTrustProxyFn(trustProxy)99 100 // This is a more optimized version of decoration101 _Request[kHasBeenDecorated] = true102 103 Object.defineProperties(_Request.prototype, {104 ip: {105 get () {106 const addrs = proxyAddr.all(this.raw, proxyFn)107 return addrs[addrs.length - 1]108 }109 },110 ips: {111 get () {112 return proxyAddr.all(this.raw, proxyFn)113 }114 },115 host: {116 get () {117 if (this.ip !== undefined && this.headers['x-forwarded-host']) {118 return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-host'])119 }120 /**121 * The last fallback supports the following cases:122 * 1. http.requireHostHeader === false123 * 2. HTTP/1.0 without a Host Header124 * 3. Headers schema that may remove the Host Header125 */126 return this.headers.host ?? this.headers[':authority'] ?? ''127 }128 },129 protocol: {130 get () {131 if (this.headers['x-forwarded-proto']) {132 return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto'])133 }134 if (this.socket) {135 return this.socket.encrypted ? 'https' : 'http'136 }137 }138 }139 })140 141 return _Request142}143 144Object.defineProperties(Request.prototype, {145 server: {146 get () {147 return this[kRouteContext].server148 }149 },150 url: {151 get () {152 return this.raw.url153 }154 },155 originalUrl: {156 get () {157 /* istanbul ignore else */158 if (!this[kRequestOriginalUrl]) {159 this[kRequestOriginalUrl] = this.raw.originalUrl || this.raw.url160 }161 return this[kRequestOriginalUrl]162 }163 },164 method: {165 get () {166 return this.raw.method167 }168 },169 routeOptions: {170 get () {171 const context = this[kRouteContext]172 const routeLimit = context._parserOptions.limit173 const serverLimit = context.server.initialConfig.bodyLimit174 const version = context.server.hasConstraintStrategy('version') ? this.raw.headers['accept-version'] : undefined175 const options = {176 method: context.config?.method,177 url: context.config?.url,178 bodyLimit: (routeLimit || serverLimit),179 attachValidation: context.attachValidation,180 logLevel: context.logLevel,181 exposeHeadRoute: context.exposeHeadRoute,182 prefixTrailingSlash: context.prefixTrailingSlash,183 handler: context.handler,184 version185 }186 187 Object.defineProperties(options, {188 config: {189 get: () => context.config190 },191 schema: {192 get: () => context.schema193 }194 })195 196 return Object.freeze(options)197 }198 },199 is404: {200 get () {201 return this[kRouteContext].config?.url === undefined202 }203 },204 socket: {205 get () {206 return this.raw.socket207 }208 },209 ip: {210 get () {211 if (this.socket) {212 return this.socket.remoteAddress213 }214 }215 },216 host: {217 get () {218 /**219 * The last fallback supports the following cases:220 * 1. http.requireHostHeader === false221 * 2. HTTP/1.0 without a Host Header222 * 3. Headers schema that may remove the Host Header223 */224 return this.raw.headers.host ?? this.raw.headers[':authority'] ?? ''225 }226 },227 hostname: {228 get () {229 return this.host.split(':', 1)[0]230 }231 },232 port: {233 get () {234 // first try taking port from host235 const portFromHost = parseInt(this.host.split(':').slice(-1)[0])236 if (!isNaN(portFromHost)) {237 return portFromHost238 }239 // now fall back to port from host/:authority header240 const host = (this.headers.host ?? this.headers[':authority'] ?? '')241 const portFromHeader = parseInt(host.split(':').slice(-1)[0])242 if (!isNaN(portFromHeader)) {243 return portFromHeader244 }245 // fall back to null246 return null247 }248 },249 protocol: {250 get () {251 if (this.socket) {252 return this.socket.encrypted ? 'https' : 'http'253 }254 }255 },256 headers: {257 get () {258 if (this.additionalHeaders) {259 return Object.assign({}, this.raw.headers, this.additionalHeaders)260 }261 return this.raw.headers262 },263 set (headers) {264 this.additionalHeaders = headers265 }266 },267 getValidationFunction: {268 value: function (httpPartOrSchema) {269 if (typeof httpPartOrSchema === 'string') {270 const symbol = HTTP_PART_SYMBOL_MAP[httpPartOrSchema]271 return this[kRouteContext][symbol]272 } else if (typeof httpPartOrSchema === 'object') {273 return this[kRouteContext][kRequestCacheValidateFns]?.get(httpPartOrSchema)274 }275 }276 },277 compileValidationSchema: {278 value: function (schema, httpPart = null) {279 const { method, url } = this280 281 if (this[kRouteContext][kRequestCacheValidateFns]?.has(schema)) {282 return this[kRouteContext][kRequestCacheValidateFns].get(schema)283 }284 285 const validatorCompiler = this[kRouteContext].validatorCompiler ||286 this.server[kSchemaController].validatorCompiler ||287 (288 // We compile the schemas if no custom validatorCompiler is provided289 // nor set290 this.server[kSchemaController].setupValidator(this.server[kOptions]) ||291 this.server[kSchemaController].validatorCompiler292 )293 294 const validateFn = validatorCompiler({295 schema,296 method,297 url,298 httpPart299 })300 301 // We create a WeakMap to compile the schema only once302 // Its done lazily to avoid add overhead by creating the WeakMap303 // if it is not used304 // TODO: Explore a central cache for all the schemas shared across305 // encapsulated contexts306 if (this[kRouteContext][kRequestCacheValidateFns] == null) {307 this[kRouteContext][kRequestCacheValidateFns] = new WeakMap()308 }309 310 this[kRouteContext][kRequestCacheValidateFns].set(schema, validateFn)311 312 return validateFn313 }314 },315 validateInput: {316 value: function (input, schema, httpPart) {317 httpPart = typeof schema === 'string' ? schema : httpPart318 319 const symbol = (httpPart != null && typeof httpPart === 'string') && HTTP_PART_SYMBOL_MAP[httpPart]320 let validate321 322 if (symbol) {323 // Validate using the HTTP Request Part schema324 validate = this[kRouteContext][symbol]325 }326 327 // We cannot compile if the schema is missed328 if (validate == null && (schema == null ||329 typeof schema !== 'object' ||330 Array.isArray(schema))331 ) {332 throw new FST_ERR_REQ_INVALID_VALIDATION_INVOCATION(httpPart)333 }334 335 if (validate == null) {336 if (this[kRouteContext][kRequestCacheValidateFns]?.has(schema)) {337 validate = this[kRouteContext][kRequestCacheValidateFns].get(schema)338 } else {339 // We proceed to compile if there's no validate function yet340 validate = this.compileValidationSchema(schema, httpPart)341 }342 }343 344 return validate(input)345 }346 }347})348 349module.exports = Request350module.exports.buildRequest = buildRequest351 