UNPKG

9.2 kBJavaScriptView Raw
1'use strict'
2
3const proxyAddr = require('proxy-addr')
4const semver = require('semver')
5const {
6 FSTDEP005,
7 FSTDEP012,
8 FSTDEP015,
9 FSTDEP016,
10 FSTDEP017,
11 FSTDEP018
12} = require('./warnings')
13const {
14 kHasBeenDecorated,
15 kSchemaBody,
16 kSchemaHeaders,
17 kSchemaParams,
18 kSchemaQuerystring,
19 kSchemaController,
20 kOptions,
21 kRequestCacheValidateFns,
22 kRouteContext,
23 kPublicRouteContext,
24 kRequestOriginalUrl
25} = require('./symbols')
26const { FST_ERR_REQ_INVALID_VALIDATION_INVOCATION } = require('./errors')
27
28const HTTP_PART_SYMBOL_MAP = {
29 body: kSchemaBody,
30 headers: kSchemaHeaders,
31 params: kSchemaParams,
32 querystring: kSchemaQuerystring,
33 query: kSchemaQuerystring
34}
35
36function Request (id, params, req, query, log, context) {
37 this.id = id
38 this[kRouteContext] = context
39 this.params = params
40 this.raw = req
41 this.query = query
42 this.log = log
43 this.body = undefined
44}
45Request.props = []
46
47function getTrustProxyFn (tp) {
48 if (typeof tp === 'function') {
49 return tp
50 }
51 if (tp === true) {
52 // Support plain true/false
53 return function () { return true }
54 }
55 if (typeof tp === 'number') {
56 // Support trusting hop count
57 return function (a, i) { return i < tp }
58 }
59 if (typeof tp === 'string') {
60 // Support comma-separated tps
61 const values = tp.split(',').map(it => it.trim())
62 return proxyAddr.compile(values)
63 }
64 return proxyAddr.compile(tp)
65}
66
67function buildRequest (R, trustProxy) {
68 if (trustProxy) {
69 return buildRequestWithTrustProxy(R, trustProxy)
70 }
71
72 return buildRegularRequest(R)
73}
74
75function buildRegularRequest (R) {
76 const props = R.props.slice()
77 function _Request (id, params, req, query, log, context) {
78 this.id = id
79 this[kRouteContext] = context
80 this.params = params
81 this.raw = req
82 this.query = query
83 this.log = log
84 this.body = undefined
85
86 // eslint-disable-next-line no-var
87 var prop
88 // eslint-disable-next-line no-var
89 for (var i = 0; i < props.length; i++) {
90 prop = props[i]
91 this[prop.key] = prop.value
92 }
93 }
94 Object.setPrototypeOf(_Request.prototype, R.prototype)
95 Object.setPrototypeOf(_Request, R)
96 _Request.props = props
97 _Request.parent = R
98
99 return _Request
100}
101
102function getLastEntryInMultiHeaderValue (headerValue) {
103 // we use the last one if the header is set more than once
104 const lastIndex = headerValue.lastIndexOf(',')
105 return lastIndex === -1 ? headerValue.trim() : headerValue.slice(lastIndex + 1).trim()
106}
107
108function buildRequestWithTrustProxy (R, trustProxy) {
109 const _Request = buildRegularRequest(R)
110 const proxyFn = getTrustProxyFn(trustProxy)
111
112 // This is a more optimized version of decoration
113 _Request[kHasBeenDecorated] = true
114
115 Object.defineProperties(_Request.prototype, {
116 ip: {
117 get () {
118 return proxyAddr(this.raw, proxyFn)
119 }
120 },
121 ips: {
122 get () {
123 return proxyAddr.all(this.raw, proxyFn)
124 }
125 },
126 hostname: {
127 get () {
128 if (this.ip !== undefined && this.headers['x-forwarded-host']) {
129 return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-host'])
130 }
131 return this.headers.host || this.headers[':authority']
132 }
133 },
134 protocol: {
135 get () {
136 if (this.headers['x-forwarded-proto']) {
137 return getLastEntryInMultiHeaderValue(this.headers['x-forwarded-proto'])
138 }
139 if (this.socket) {
140 return this.socket.encrypted ? 'https' : 'http'
141 }
142 }
143 }
144 })
145
146 return _Request
147}
148
149Object.defineProperties(Request.prototype, {
150 server: {
151 get () {
152 return this[kRouteContext].server
153 }
154 },
155 url: {
156 get () {
157 return this.raw.url
158 }
159 },
160 originalUrl: {
161 get () {
162 /* istanbul ignore else */
163 if (!this[kRequestOriginalUrl]) {
164 this[kRequestOriginalUrl] = this.raw.originalUrl || this.raw.url
165 }
166 return this[kRequestOriginalUrl]
167 }
168 },
169 method: {
170 get () {
171 return this.raw.method
172 }
173 },
174 context: {
175 get () {
176 FSTDEP012()
177 return this[kRouteContext]
178 }
179 },
180 routerPath: {
181 get () {
182 FSTDEP017()
183 return this[kRouteContext].config?.url
184 }
185 },
186 routeOptions: {
187 get () {
188 const context = this[kRouteContext]
189 const routeLimit = context._parserOptions.limit
190 const serverLimit = context.server.initialConfig.bodyLimit
191 const version = context.server.hasConstraintStrategy('version') ? this.raw.headers['accept-version'] : undefined
192 const options = {
193 method: context.config?.method,
194 url: context.config?.url,
195 bodyLimit: (routeLimit || serverLimit),
196 attachValidation: context.attachValidation,
197 logLevel: context.logLevel,
198 exposeHeadRoute: context.exposeHeadRoute,
199 prefixTrailingSlash: context.prefixTrailingSlash,
200 handler: context.handler,
201 version
202 }
203
204 Object.defineProperties(options, {
205 config: {
206 get: () => context.config
207 },
208 schema: {
209 get: () => context.schema
210 }
211 })
212
213 return Object.freeze(options)
214 }
215 },
216 routerMethod: {
217 get () {
218 FSTDEP018()
219 return this[kRouteContext].config?.method
220 }
221 },
222 routeConfig: {
223 get () {
224 FSTDEP016()
225 return this[kRouteContext][kPublicRouteContext]?.config
226 }
227 },
228 routeSchema: {
229 get () {
230 FSTDEP015()
231 return this[kRouteContext][kPublicRouteContext].schema
232 }
233 },
234 is404: {
235 get () {
236 return this[kRouteContext].config?.url === undefined
237 }
238 },
239 connection: {
240 get () {
241 /* istanbul ignore next */
242 if (semver.gte(process.versions.node, '13.0.0')) {
243 FSTDEP005()
244 }
245 return this.raw.connection
246 }
247 },
248 socket: {
249 get () {
250 return this.raw.socket
251 }
252 },
253 ip: {
254 get () {
255 if (this.socket) {
256 return this.socket.remoteAddress
257 }
258 }
259 },
260 hostname: {
261 get () {
262 return this.raw.headers.host || this.raw.headers[':authority']
263 }
264 },
265 protocol: {
266 get () {
267 if (this.socket) {
268 return this.socket.encrypted ? 'https' : 'http'
269 }
270 }
271 },
272 headers: {
273 get () {
274 if (this.additionalHeaders) {
275 return Object.assign({}, this.raw.headers, this.additionalHeaders)
276 }
277 return this.raw.headers
278 },
279 set (headers) {
280 this.additionalHeaders = headers
281 }
282 },
283 getValidationFunction: {
284 value: function (httpPartOrSchema) {
285 if (typeof httpPartOrSchema === 'string') {
286 const symbol = HTTP_PART_SYMBOL_MAP[httpPartOrSchema]
287 return this[kRouteContext][symbol]
288 } else if (typeof httpPartOrSchema === 'object') {
289 return this[kRouteContext][kRequestCacheValidateFns]?.get(httpPartOrSchema)
290 }
291 }
292 },
293 compileValidationSchema: {
294 value: function (schema, httpPart = null) {
295 const { method, url } = this
296
297 if (this[kRouteContext][kRequestCacheValidateFns]?.has(schema)) {
298 return this[kRouteContext][kRequestCacheValidateFns].get(schema)
299 }
300
301 const validatorCompiler = this[kRouteContext].validatorCompiler ||
302 this.server[kSchemaController].validatorCompiler ||
303 (
304 // We compile the schemas if no custom validatorCompiler is provided
305 // nor set
306 this.server[kSchemaController].setupValidator(this.server[kOptions]) ||
307 this.server[kSchemaController].validatorCompiler
308 )
309
310 const validateFn = validatorCompiler({
311 schema,
312 method,
313 url,
314 httpPart
315 })
316
317 // We create a WeakMap to compile the schema only once
318 // Its done lazily to avoid add overhead by creating the WeakMap
319 // if it is not used
320 // TODO: Explore a central cache for all the schemas shared across
321 // encapsulated contexts
322 if (this[kRouteContext][kRequestCacheValidateFns] == null) {
323 this[kRouteContext][kRequestCacheValidateFns] = new WeakMap()
324 }
325
326 this[kRouteContext][kRequestCacheValidateFns].set(schema, validateFn)
327
328 return validateFn
329 }
330 },
331 validateInput: {
332 value: function (input, schema, httpPart) {
333 httpPart = typeof schema === 'string' ? schema : httpPart
334
335 const symbol = (httpPart != null && typeof httpPart === 'string') && HTTP_PART_SYMBOL_MAP[httpPart]
336 let validate
337
338 if (symbol) {
339 // Validate using the HTTP Request Part schema
340 validate = this[kRouteContext][symbol]
341 }
342
343 // We cannot compile if the schema is missed
344 if (validate == null && (schema == null ||
345 typeof schema !== 'object' ||
346 Array.isArray(schema))
347 ) {
348 throw new FST_ERR_REQ_INVALID_VALIDATION_INVOCATION(httpPart)
349 }
350
351 if (validate == null) {
352 if (this[kRouteContext][kRequestCacheValidateFns]?.has(schema)) {
353 validate = this[kRouteContext][kRequestCacheValidateFns].get(schema)
354 } else {
355 // We proceed to compile if there's no validate function yet
356 validate = this.compileValidationSchema(schema, httpPart)
357 }
358 }
359
360 return validate(input)
361 }
362 }
363})
364
365module.exports = Request
366module.exports.buildRequest = buildRequest