1 | 'use strict'
|
2 |
|
3 | const FindMyWay = require('find-my-way')
|
4 | const proxyAddr = require('proxy-addr')
|
5 | const Context = require('./context')
|
6 | const { buildMiddie, onRunMiddlewares } = require('./middleware')
|
7 | const { hookRunner, hookIterator } = require('./hooks')
|
8 | const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
|
9 | const supportedHooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization', 'onResponse', 'onSend']
|
10 | const validation = require('./validation')
|
11 | const buildSchema = validation.build
|
12 | const { buildSchemaCompiler } = validation
|
13 | const { beforeHandlerWarning } = require('./warnings')
|
14 |
|
15 | const {
|
16 | codes: {
|
17 | FST_ERR_SCH_BUILD,
|
18 | FST_ERR_SCH_MISSING_COMPILER
|
19 | }
|
20 | } = require('./errors')
|
21 |
|
22 | const {
|
23 | kRoutePrefix,
|
24 | kLogLevel,
|
25 | kLogSerializers,
|
26 | kHooks,
|
27 | kSchemas,
|
28 | kOptions,
|
29 | kSchemaCompiler,
|
30 | kSchemaResolver,
|
31 | kContentTypeParser,
|
32 | kReply,
|
33 | kReplySerializerDefault,
|
34 | kRequest,
|
35 | kMiddlewares,
|
36 | kGlobalHooks,
|
37 | kDisableRequestLogging
|
38 | } = require('./symbols.js')
|
39 |
|
40 | function buildRouting (options) {
|
41 | const router = FindMyWay(options.config)
|
42 |
|
43 | const schemaCache = new Map()
|
44 | schemaCache.put = schemaCache.set
|
45 |
|
46 | let avvio
|
47 | let fourOhFour
|
48 | let trustProxy
|
49 | let requestIdHeader
|
50 | let querystringParser
|
51 | let requestIdLogLabel
|
52 | let logger
|
53 | let hasLogger
|
54 | let setupResponseListeners
|
55 | let throwIfAlreadyStarted
|
56 | let proxyFn
|
57 | let modifyCoreObjects
|
58 | let genReqId
|
59 | let disableRequestLogging
|
60 | let ignoreTrailingSlash
|
61 | let return503OnClosing
|
62 |
|
63 | let closing = false
|
64 |
|
65 | return {
|
66 | setup (options, fastifyArgs) {
|
67 | avvio = fastifyArgs.avvio
|
68 | fourOhFour = fastifyArgs.fourOhFour
|
69 | logger = fastifyArgs.logger
|
70 | hasLogger = fastifyArgs.hasLogger
|
71 | setupResponseListeners = fastifyArgs.setupResponseListeners
|
72 | throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted
|
73 |
|
74 | proxyFn = getTrustProxyFn(options)
|
75 | trustProxy = options.trustProxy
|
76 | requestIdHeader = options.requestIdHeader
|
77 | querystringParser = options.querystringParser
|
78 | requestIdLogLabel = options.requestIdLogLabel
|
79 | modifyCoreObjects = options.modifyCoreObjects
|
80 | genReqId = options.genReqId
|
81 | disableRequestLogging = options.disableRequestLogging
|
82 | ignoreTrailingSlash = options.ignoreTrailingSlash
|
83 | return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true
|
84 | },
|
85 | routing: router.lookup.bind(router),
|
86 | route,
|
87 | prepareRoute,
|
88 | routeHandler,
|
89 | closeRoutes: () => { closing = true },
|
90 | printRoutes: router.prettyPrint.bind(router)
|
91 | }
|
92 |
|
93 |
|
94 | function prepareRoute (method, url, options, handler) {
|
95 | if (!handler && typeof options === 'function') {
|
96 | handler = options
|
97 | options = {}
|
98 | } else if (handler && typeof handler === 'function') {
|
99 | if (Object.prototype.toString.call(options) !== '[object Object]') {
|
100 | throw new Error(`Options for ${method}:${url} route must be an object`)
|
101 | } else if (options.handler) {
|
102 | if (typeof options.handler === 'function') {
|
103 | throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`)
|
104 | } else {
|
105 | throw new Error(`Handler for ${method}:${url} route must be a function`)
|
106 | }
|
107 | }
|
108 | }
|
109 |
|
110 | options = Object.assign({}, options, {
|
111 | method,
|
112 | url,
|
113 | handler: handler || (options && options.handler)
|
114 | })
|
115 |
|
116 | return route.call(this, options)
|
117 | }
|
118 |
|
119 |
|
120 | function route (opts) {
|
121 | throwIfAlreadyStarted('Cannot add route when fastify instance is already started!')
|
122 |
|
123 | if (Array.isArray(opts.method)) {
|
124 | for (var i = 0; i < opts.method.length; i++) {
|
125 | if (supportedMethods.indexOf(opts.method[i]) === -1) {
|
126 | throw new Error(`${opts.method[i]} method is not supported!`)
|
127 | }
|
128 | }
|
129 | } else {
|
130 | if (supportedMethods.indexOf(opts.method) === -1) {
|
131 | throw new Error(`${opts.method} method is not supported!`)
|
132 | }
|
133 | }
|
134 |
|
135 | if (!opts.handler) {
|
136 | throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`)
|
137 | }
|
138 |
|
139 | validateBodyLimitOption(opts.bodyLimit)
|
140 |
|
141 | if (opts.preHandler == null && opts.beforeHandler != null) {
|
142 | beforeHandlerWarning()
|
143 | opts.preHandler = opts.beforeHandler
|
144 | }
|
145 |
|
146 | const prefix = this[kRoutePrefix]
|
147 |
|
148 | this.after((notHandledErr, done) => {
|
149 | var path = opts.url || opts.path
|
150 | if (path === '/' && prefix.length > 0) {
|
151 | switch (opts.prefixTrailingSlash) {
|
152 | case 'slash':
|
153 | afterRouteAdded.call(this, path, notHandledErr, done)
|
154 | break
|
155 | case 'no-slash':
|
156 | afterRouteAdded.call(this, '', notHandledErr, done)
|
157 | break
|
158 | case 'both':
|
159 | default:
|
160 | afterRouteAdded.call(this, '', notHandledErr, done)
|
161 |
|
162 | if (ignoreTrailingSlash !== true) {
|
163 | afterRouteAdded.call(this, path, notHandledErr, done)
|
164 | }
|
165 | }
|
166 | } else if (path[0] === '/' && prefix.endsWith('/')) {
|
167 |
|
168 | afterRouteAdded.call(this, path.slice(1), notHandledErr, done)
|
169 | } else {
|
170 | afterRouteAdded.call(this, path, notHandledErr, done)
|
171 | }
|
172 | })
|
173 |
|
174 |
|
175 | return this
|
176 |
|
177 | function afterRouteAdded (path, notHandledErr, done) {
|
178 | const url = prefix + path
|
179 |
|
180 | opts.url = url
|
181 | opts.path = url
|
182 | opts.prefix = prefix
|
183 | opts.logLevel = opts.logLevel || this[kLogLevel]
|
184 |
|
185 | if (this[kLogSerializers] || opts.logSerializers) {
|
186 | opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers)
|
187 | }
|
188 |
|
189 | if (opts.attachValidation == null) {
|
190 | opts.attachValidation = false
|
191 | }
|
192 |
|
193 |
|
194 | for (const hook of this[kGlobalHooks].onRoute) {
|
195 | try {
|
196 | hook.call(this, opts)
|
197 | } catch (error) {
|
198 | done(error)
|
199 | return
|
200 | }
|
201 | }
|
202 |
|
203 | const config = opts.config || {}
|
204 | config.url = url
|
205 |
|
206 | const context = new Context(
|
207 | opts.schema,
|
208 | opts.handler.bind(this),
|
209 | this[kReply],
|
210 | this[kRequest],
|
211 | this[kContentTypeParser],
|
212 | config,
|
213 | this._errorHandler,
|
214 | opts.bodyLimit,
|
215 | opts.logLevel,
|
216 | opts.logSerializers,
|
217 | opts.attachValidation,
|
218 | this[kReplySerializerDefault]
|
219 | )
|
220 |
|
221 | for (const hook of supportedHooks) {
|
222 | if (opts[hook]) {
|
223 | if (Array.isArray(opts[hook])) {
|
224 | opts[hook] = opts[hook].map(fn => fn.bind(this))
|
225 | } else {
|
226 | opts[hook] = opts[hook].bind(this)
|
227 | }
|
228 | }
|
229 | }
|
230 |
|
231 | try {
|
232 | router.on(opts.method, opts.url, { version: opts.version }, routeHandler, context)
|
233 | } catch (err) {
|
234 | done(err)
|
235 | return
|
236 | }
|
237 |
|
238 |
|
239 |
|
240 |
|
241 | avvio.once('preReady', () => {
|
242 | const onResponse = this[kHooks].onResponse
|
243 | const onSend = this[kHooks].onSend
|
244 | const onError = this[kHooks].onError
|
245 |
|
246 | context.onSend = onSend.length ? onSend : null
|
247 | context.onError = onError.length ? onError : null
|
248 | context.onResponse = onResponse.length ? onResponse : null
|
249 |
|
250 | for (const hook of supportedHooks) {
|
251 | const toSet = this[kHooks][hook].concat(opts[hook] || [])
|
252 | context[hook] = toSet.length ? toSet : null
|
253 | }
|
254 |
|
255 | context._middie = buildMiddie(this[kMiddlewares])
|
256 |
|
257 |
|
258 |
|
259 | fourOhFour.setContext(this, context)
|
260 |
|
261 | if (opts.schema) {
|
262 | if (this[kSchemaCompiler] == null && this[kSchemaResolver]) {
|
263 | throw new FST_ERR_SCH_MISSING_COMPILER(opts.method, url)
|
264 | }
|
265 |
|
266 | try {
|
267 | if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) {
|
268 | const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true })
|
269 | this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, this[kOptions].ajv, schemaCache))
|
270 | }
|
271 |
|
272 | buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas], this[kSchemaResolver])
|
273 | } catch (error) {
|
274 |
|
275 | throw (error.code ? error : new FST_ERR_SCH_BUILD(opts.method, url, error.message))
|
276 | }
|
277 | }
|
278 | })
|
279 |
|
280 | done(notHandledErr)
|
281 | }
|
282 | }
|
283 |
|
284 |
|
285 | function routeHandler (req, res, params, context) {
|
286 | if (closing === true) {
|
287 | if (req.httpVersionMajor !== 2) {
|
288 | res.once('finish', () => req.destroy())
|
289 | res.setHeader('Connection', 'close')
|
290 | }
|
291 |
|
292 | if (return503OnClosing) {
|
293 | const headers = {
|
294 | 'Content-Type': 'application/json',
|
295 | 'Content-Length': '80'
|
296 | }
|
297 | res.writeHead(503, headers)
|
298 | res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}')
|
299 | return
|
300 | }
|
301 | }
|
302 |
|
303 | req.id = req.headers[requestIdHeader] || genReqId(req)
|
304 | req.originalUrl = req.url
|
305 | var hostname = req.headers.host || req.headers[':authority']
|
306 | var ip = req.connection.remoteAddress
|
307 | var ips
|
308 |
|
309 | if (trustProxy) {
|
310 | ip = proxyAddr(req, proxyFn)
|
311 | ips = proxyAddr.all(req, proxyFn)
|
312 | if (ip !== undefined && req.headers['x-forwarded-host']) {
|
313 | hostname = req.headers['x-forwarded-host']
|
314 | }
|
315 | }
|
316 |
|
317 | var loggerOpts = {
|
318 | [requestIdLogLabel]: req.id,
|
319 | level: context.logLevel
|
320 | }
|
321 |
|
322 | if (context.logSerializers) {
|
323 | loggerOpts.serializers = context.logSerializers
|
324 | }
|
325 | var childLogger = logger.child(loggerOpts)
|
326 | childLogger[kDisableRequestLogging] = disableRequestLogging
|
327 |
|
328 |
|
329 | if (modifyCoreObjects) {
|
330 | req.hostname = hostname
|
331 | req.ip = ip
|
332 | req.ips = ips
|
333 |
|
334 | req.log = res.log = childLogger
|
335 | }
|
336 |
|
337 | if (disableRequestLogging === false) {
|
338 | childLogger.info({ req }, 'incoming request')
|
339 | }
|
340 |
|
341 | var queryPrefix = req.url.indexOf('?')
|
342 | var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '')
|
343 | var request = new context.Request(params, req, query, req.headers, childLogger, ip, ips, hostname)
|
344 | var reply = new context.Reply(res, context, request, childLogger)
|
345 |
|
346 | if (hasLogger === true || context.onResponse !== null) {
|
347 | setupResponseListeners(reply)
|
348 | }
|
349 |
|
350 | if (context.onRequest !== null) {
|
351 | hookRunner(
|
352 | context.onRequest,
|
353 | hookIterator,
|
354 | request,
|
355 | reply,
|
356 | middlewareCallback
|
357 | )
|
358 | } else {
|
359 | middlewareCallback(null, request, reply)
|
360 | }
|
361 | }
|
362 | }
|
363 |
|
364 | function validateBodyLimitOption (bodyLimit) {
|
365 | if (bodyLimit === undefined) return
|
366 | if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) {
|
367 | throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`)
|
368 | }
|
369 | }
|
370 |
|
371 | function middlewareCallback (err, request, reply) {
|
372 | if (reply.sent === true) return
|
373 | if (err != null) {
|
374 | reply.send(err)
|
375 | return
|
376 | }
|
377 |
|
378 | if (reply.context._middie !== null) {
|
379 | reply.context._middie.run(request.raw, reply.res, reply)
|
380 | } else {
|
381 | onRunMiddlewares(null, null, null, reply)
|
382 | }
|
383 | }
|
384 |
|
385 | function getTrustProxyFn (options) {
|
386 | const tp = options.trustProxy
|
387 | if (typeof tp === 'function') {
|
388 | return tp
|
389 | }
|
390 | if (tp === true) {
|
391 |
|
392 | return function () { return true }
|
393 | }
|
394 | if (typeof tp === 'number') {
|
395 |
|
396 | return function (a, i) { return i < tp }
|
397 | }
|
398 | if (typeof tp === 'string') {
|
399 |
|
400 | const vals = tp.split(',').map(it => it.trim())
|
401 | return proxyAddr.compile(vals)
|
402 | }
|
403 | return proxyAddr.compile(tp || [])
|
404 | }
|
405 |
|
406 | module.exports = { buildRouting, validateBodyLimitOption }
|