You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					517 lines
				
				16 KiB
			
		
		
			
		
	
	
					517 lines
				
				16 KiB
			| 
											3 years ago
										 | 'use strict' | ||
|  | 
 | ||
|  | const FindMyWay = require('find-my-way') | ||
|  | const Context = require('./context') | ||
|  | const handleRequest = require('./handleRequest') | ||
|  | const { hookRunner, hookIterator, lifecycleHooks } = require('./hooks') | ||
|  | const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS'] | ||
|  | const { normalizeSchema } = require('./schemas') | ||
|  | const { parseHeadOnSendHandlers } = require('./headRoute') | ||
|  | const warning = require('./warnings') | ||
|  | const { kRequestAcceptVersion } = require('./symbols') | ||
|  | 
 | ||
|  | const { | ||
|  |   compileSchemasForValidation, | ||
|  |   compileSchemasForSerialization | ||
|  | } = require('./validation') | ||
|  | 
 | ||
|  | const { | ||
|  |   FST_ERR_SCH_VALIDATION_BUILD, | ||
|  |   FST_ERR_SCH_SERIALIZATION_BUILD, | ||
|  |   FST_ERR_DEFAULT_ROUTE_INVALID_TYPE, | ||
|  |   FST_ERR_INVALID_URL | ||
|  | } = require('./errors') | ||
|  | 
 | ||
|  | const { | ||
|  |   kRoutePrefix, | ||
|  |   kLogLevel, | ||
|  |   kLogSerializers, | ||
|  |   kHooks, | ||
|  |   kHooksDeprecatedPreParsing, | ||
|  |   kSchemaController, | ||
|  |   kOptions, | ||
|  |   kContentTypeParser, | ||
|  |   kReply, | ||
|  |   kReplySerializerDefault, | ||
|  |   kReplyIsError, | ||
|  |   kRequest, | ||
|  |   kRequestPayloadStream, | ||
|  |   kDisableRequestLogging, | ||
|  |   kSchemaErrorFormatter, | ||
|  |   kErrorHandler | ||
|  | } = require('./symbols.js') | ||
|  | 
 | ||
|  | function buildRouting (options) { | ||
|  |   const { keepAliveConnections } = options | ||
|  |   const router = FindMyWay(options.config) | ||
|  | 
 | ||
|  |   let avvio | ||
|  |   let fourOhFour | ||
|  |   let requestIdHeader | ||
|  |   let querystringParser | ||
|  |   let requestIdLogLabel | ||
|  |   let logger | ||
|  |   let hasLogger | ||
|  |   let setupResponseListeners | ||
|  |   let throwIfAlreadyStarted | ||
|  |   let genReqId | ||
|  |   let disableRequestLogging | ||
|  |   let ignoreTrailingSlash | ||
|  |   let return503OnClosing | ||
|  |   let globalExposeHeadRoutes | ||
|  | 
 | ||
|  |   let closing = false | ||
|  | 
 | ||
|  |   return { | ||
|  |     setup (options, fastifyArgs) { | ||
|  |       avvio = fastifyArgs.avvio | ||
|  |       fourOhFour = fastifyArgs.fourOhFour | ||
|  |       logger = fastifyArgs.logger | ||
|  |       hasLogger = fastifyArgs.hasLogger | ||
|  |       setupResponseListeners = fastifyArgs.setupResponseListeners | ||
|  |       throwIfAlreadyStarted = fastifyArgs.throwIfAlreadyStarted | ||
|  | 
 | ||
|  |       globalExposeHeadRoutes = options.exposeHeadRoutes | ||
|  |       requestIdHeader = options.requestIdHeader | ||
|  |       querystringParser = options.querystringParser | ||
|  |       requestIdLogLabel = options.requestIdLogLabel | ||
|  |       genReqId = options.genReqId | ||
|  |       disableRequestLogging = options.disableRequestLogging | ||
|  |       ignoreTrailingSlash = options.ignoreTrailingSlash | ||
|  |       return503OnClosing = Object.prototype.hasOwnProperty.call(options, 'return503OnClosing') ? options.return503OnClosing : true | ||
|  |     }, | ||
|  |     routing: router.lookup.bind(router), // router func to find the right handler to call
 | ||
|  |     route, // configure a route in the fastify instance
 | ||
|  |     prepareRoute, | ||
|  |     getDefaultRoute: function () { | ||
|  |       return router.defaultRoute | ||
|  |     }, | ||
|  |     setDefaultRoute: function (defaultRoute) { | ||
|  |       if (typeof defaultRoute !== 'function') { | ||
|  |         throw new FST_ERR_DEFAULT_ROUTE_INVALID_TYPE() | ||
|  |       } | ||
|  | 
 | ||
|  |       router.defaultRoute = defaultRoute | ||
|  |     }, | ||
|  |     routeHandler, | ||
|  |     closeRoutes: () => { closing = true }, | ||
|  |     printRoutes: router.prettyPrint.bind(router) | ||
|  |   } | ||
|  | 
 | ||
|  |   // Convert shorthand to extended route declaration
 | ||
|  |   function prepareRoute (method, url, options, handler) { | ||
|  |     if (typeof url !== 'string') { | ||
|  |       throw new FST_ERR_INVALID_URL(typeof url) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (!handler && typeof options === 'function') { | ||
|  |       handler = options // for support over direct function calls such as fastify.get() options are reused as the handler
 | ||
|  |       options = {} | ||
|  |     } else if (handler && typeof handler === 'function') { | ||
|  |       if (Object.prototype.toString.call(options) !== '[object Object]') { | ||
|  |         throw new Error(`Options for ${method}:${url} route must be an object`) | ||
|  |       } else if (options.handler) { | ||
|  |         if (typeof options.handler === 'function') { | ||
|  |           throw new Error(`Duplicate handler for ${method}:${url} route is not allowed!`) | ||
|  |         } else { | ||
|  |           throw new Error(`Handler for ${method}:${url} route must be a function`) | ||
|  |         } | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     options = Object.assign({}, options, { | ||
|  |       method, | ||
|  |       url, | ||
|  |       path: url, | ||
|  |       handler: handler || (options && options.handler) | ||
|  |     }) | ||
|  | 
 | ||
|  |     return route.call(this, options) | ||
|  |   } | ||
|  | 
 | ||
|  |   // Route management
 | ||
|  |   function route (options) { | ||
|  |     // Since we are mutating/assigning only top level props, it is fine to have a shallow copy using the spread operator
 | ||
|  |     const opts = { ...options } | ||
|  | 
 | ||
|  |     throwIfAlreadyStarted('Cannot add route when fastify instance is already started!') | ||
|  | 
 | ||
|  |     if (Array.isArray(opts.method)) { | ||
|  |       // eslint-disable-next-line no-var
 | ||
|  |       for (var i = 0; i < opts.method.length; ++i) { | ||
|  |         const method = opts.method[i] | ||
|  |         if (supportedMethods.indexOf(method) === -1) { | ||
|  |           throw new Error(`${method} method is not supported!`) | ||
|  |         } | ||
|  |       } | ||
|  |     } else { | ||
|  |       if (supportedMethods.indexOf(opts.method) === -1) { | ||
|  |         throw new Error(`${opts.method} method is not supported!`) | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     const path = opts.url || opts.path | ||
|  | 
 | ||
|  |     if (!opts.handler) { | ||
|  |       throw new Error(`Missing handler function for ${opts.method}:${path} route.`) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (opts.errorHandler !== undefined && typeof opts.errorHandler !== 'function') { | ||
|  |       throw new Error(`Error Handler for ${opts.method}:${path} route, if defined, must be a function`) | ||
|  |     } | ||
|  | 
 | ||
|  |     validateBodyLimitOption(opts.bodyLimit) | ||
|  | 
 | ||
|  |     const prefix = this[kRoutePrefix] | ||
|  | 
 | ||
|  |     this.after((notHandledErr, done) => { | ||
|  |       if (path === '/' && prefix.length && opts.method !== 'HEAD') { | ||
|  |         switch (opts.prefixTrailingSlash) { | ||
|  |           case 'slash': | ||
|  |             afterRouteAdded.call(this, { path }, notHandledErr, done) | ||
|  |             break | ||
|  |           case 'no-slash': | ||
|  |             afterRouteAdded.call(this, { path: '' }, notHandledErr, done) | ||
|  |             break | ||
|  |           case 'both': | ||
|  |           default: | ||
|  |             afterRouteAdded.call(this, { path: '' }, notHandledErr, done) | ||
|  |             // If ignoreTrailingSlash is set to true we need to add only the '' route to prevent adding an incomplete one.
 | ||
|  |             if (ignoreTrailingSlash !== true) { | ||
|  |               afterRouteAdded.call(this, { path, prefixing: true }, notHandledErr, done) | ||
|  |             } | ||
|  |         } | ||
|  |       } else if (path && path[0] === '/' && prefix.endsWith('/')) { | ||
|  |         // Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
 | ||
|  |         afterRouteAdded.call(this, { path: path.slice(1) }, notHandledErr, done) | ||
|  |       } else { | ||
|  |         afterRouteAdded.call(this, { path }, notHandledErr, done) | ||
|  |       } | ||
|  |     }) | ||
|  | 
 | ||
|  |     // chainable api
 | ||
|  |     return this | ||
|  | 
 | ||
|  |     /** | ||
|  |      * This function sets up a new route, its log serializers, and triggers route hooks. | ||
|  |      * | ||
|  |      * @param {object} opts contains route `path` and `prefixing` flag which indicates if this is an auto-prefixed route, e.g. `fastify.register(routes, { prefix: '/foo' })` | ||
|  |      * @param {*} notHandledErr error object to be passed back to the original invoker | ||
|  |      * @param {*} done callback | ||
|  |      */ | ||
|  |     function afterRouteAdded ({ path, prefixing = false }, notHandledErr, done) { | ||
|  |       const url = prefix + path | ||
|  | 
 | ||
|  |       opts.url = url | ||
|  |       opts.path = url | ||
|  |       opts.routePath = path | ||
|  |       opts.prefix = prefix | ||
|  |       opts.logLevel = opts.logLevel || this[kLogLevel] | ||
|  | 
 | ||
|  |       if (this[kLogSerializers] || opts.logSerializers) { | ||
|  |         opts.logSerializers = Object.assign(Object.create(this[kLogSerializers]), opts.logSerializers) | ||
|  |       } | ||
|  | 
 | ||
|  |       if (opts.attachValidation == null) { | ||
|  |         opts.attachValidation = false | ||
|  |       } | ||
|  | 
 | ||
|  |       if (prefixing === false) { | ||
|  |       // run 'onRoute' hooks
 | ||
|  |         for (const hook of this[kHooks].onRoute) { | ||
|  |           try { | ||
|  |             hook.call(this, opts) | ||
|  |           } catch (error) { | ||
|  |             done(error) | ||
|  |             return | ||
|  |           } | ||
|  |         } | ||
|  |       } | ||
|  | 
 | ||
|  |       const config = { | ||
|  |         ...opts.config, | ||
|  |         url, | ||
|  |         method: opts.method | ||
|  |       } | ||
|  |       const constraints = opts.constraints || {} | ||
|  |       if (opts.version) { | ||
|  |         warning.emit('FSTDEP008') | ||
|  |         constraints.version = opts.version | ||
|  |       } | ||
|  | 
 | ||
|  |       const context = new Context( | ||
|  |         opts.schema, | ||
|  |         opts.handler.bind(this), | ||
|  |         this[kReply], | ||
|  |         this[kRequest], | ||
|  |         this[kContentTypeParser], | ||
|  |         config, | ||
|  |         opts.errorHandler || this[kErrorHandler], | ||
|  |         opts.bodyLimit, | ||
|  |         opts.logLevel, | ||
|  |         opts.logSerializers, | ||
|  |         opts.attachValidation, | ||
|  |         this[kReplySerializerDefault], | ||
|  |         opts.schemaErrorFormatter || this[kSchemaErrorFormatter] | ||
|  |       ) | ||
|  | 
 | ||
|  |       const headRouteExists = router.find('HEAD', url, constraints) != null | ||
|  | 
 | ||
|  |       try { | ||
|  |         router.on(opts.method, opts.url, { constraints }, routeHandler, context) | ||
|  |       } catch (err) { | ||
|  |         done(err) | ||
|  |         return | ||
|  |       } | ||
|  | 
 | ||
|  |       const { exposeHeadRoute } = opts | ||
|  |       const hasRouteExposeHeadRouteFlag = exposeHeadRoute != null | ||
|  |       const shouldExposeHead = hasRouteExposeHeadRouteFlag ? exposeHeadRoute : globalExposeHeadRoutes | ||
|  | 
 | ||
|  |       if (shouldExposeHead && options.method === 'GET' && !headRouteExists) { | ||
|  |         const onSendHandlers = parseHeadOnSendHandlers(opts.onSend) | ||
|  |         prepareRoute.call(this, 'HEAD', path, { ...opts, onSend: onSendHandlers }) | ||
|  |       } else if (headRouteExists && exposeHeadRoute) { | ||
|  |         warning.emit('FSTDEP007') | ||
|  |       } | ||
|  | 
 | ||
|  |       // It can happen that a user registers a plugin with some hooks *after*
 | ||
|  |       // the route registration. To be sure to also load those hooks,
 | ||
|  |       // we must listen for the avvio's preReady event, and update the context object accordingly.
 | ||
|  |       avvio.once('preReady', () => { | ||
|  |         for (const hook of lifecycleHooks) { | ||
|  |           const toSet = this[kHooks][hook] | ||
|  |             .concat(opts[hook] || []) | ||
|  |             .map(h => { | ||
|  |               const bound = h.bind(this) | ||
|  | 
 | ||
|  |               // Track hooks deprecation markers
 | ||
|  |               if (hook === 'preParsing') { | ||
|  |                 // Check for deprecation syntax
 | ||
|  |                 if (h.length === (h.constructor.name === 'AsyncFunction' ? 2 : 3)) { | ||
|  |                   warning.emit('FSTDEP004') | ||
|  |                   bound[kHooksDeprecatedPreParsing] = true | ||
|  |                 } | ||
|  |               } | ||
|  | 
 | ||
|  |               return bound | ||
|  |             }) | ||
|  |           context[hook] = toSet.length ? toSet : null | ||
|  |         } | ||
|  | 
 | ||
|  |         // Must store the 404 Context in 'preReady' because it is only guaranteed to
 | ||
|  |         // be available after all of the plugins and routes have been loaded.
 | ||
|  |         fourOhFour.setContext(this, context) | ||
|  | 
 | ||
|  |         if (opts.schema) { | ||
|  |           context.schema = normalizeSchema(context.schema, this.initialConfig) | ||
|  | 
 | ||
|  |           const schemaController = this[kSchemaController] | ||
|  |           if (!opts.validatorCompiler && (opts.schema.body || opts.schema.headers || opts.schema.querystring || opts.schema.params)) { | ||
|  |             schemaController.setupValidator(this[kOptions]) | ||
|  |           } | ||
|  |           try { | ||
|  |             compileSchemasForValidation(context, opts.validatorCompiler || schemaController.validatorCompiler) | ||
|  |           } catch (error) { | ||
|  |             throw new FST_ERR_SCH_VALIDATION_BUILD(opts.method, url, error.message) | ||
|  |           } | ||
|  | 
 | ||
|  |           if (opts.schema.response && !opts.serializerCompiler) { | ||
|  |             schemaController.setupSerializer(this[kOptions]) | ||
|  |           } | ||
|  |           try { | ||
|  |             compileSchemasForSerialization(context, opts.serializerCompiler || schemaController.serializerCompiler) | ||
|  |           } catch (error) { | ||
|  |             throw new FST_ERR_SCH_SERIALIZATION_BUILD(opts.method, url, error.message) | ||
|  |           } | ||
|  |         } | ||
|  |       }) | ||
|  | 
 | ||
|  |       done(notHandledErr) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   // HTTP request entry point, the routing has already been executed
 | ||
|  |   function routeHandler (req, res, params, context) { | ||
|  |     if (closing === true) { | ||
|  |       /* istanbul ignore next mac, windows */ | ||
|  |       if (req.httpVersionMajor !== 2) { | ||
|  |         res.once('finish', () => req.destroy()) | ||
|  |         res.setHeader('Connection', 'close') | ||
|  |       } | ||
|  | 
 | ||
|  |       if (return503OnClosing) { | ||
|  |         const headers = { | ||
|  |           'Content-Type': 'application/json', | ||
|  |           'Content-Length': '80' | ||
|  |         } | ||
|  |         res.writeHead(503, headers) | ||
|  |         res.end('{"error":"Service Unavailable","message":"Service Unavailable","statusCode":503}') | ||
|  |         return | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // When server.forceCloseConnections is true, we will collect any requests
 | ||
|  |     // that have indicated they want persistence so that they can be reaped
 | ||
|  |     // on server close. Otherwise, the container is a noop container.
 | ||
|  |     const connHeader = String.prototype.toLowerCase.call(req.headers.connection || '') | ||
|  |     if (connHeader === 'keep-alive') { | ||
|  |       if (keepAliveConnections.has(req.socket) === false) { | ||
|  |         keepAliveConnections.add(req.socket) | ||
|  |         req.socket.on('close', removeTrackedSocket.bind({ keepAliveConnections, socket: req.socket })) | ||
|  |       } | ||
|  |     } | ||
|  | 
 | ||
|  |     // we revert the changes in defaultRoute
 | ||
|  |     if (req.headers[kRequestAcceptVersion] !== undefined) { | ||
|  |       req.headers['accept-version'] = req.headers[kRequestAcceptVersion] | ||
|  |       req.headers[kRequestAcceptVersion] = undefined | ||
|  |     } | ||
|  | 
 | ||
|  |     const id = req.headers[requestIdHeader] || genReqId(req) | ||
|  | 
 | ||
|  |     const loggerBinding = { | ||
|  |       [requestIdLogLabel]: id | ||
|  |     } | ||
|  | 
 | ||
|  |     const loggerOpts = { | ||
|  |       level: context.logLevel | ||
|  |     } | ||
|  | 
 | ||
|  |     if (context.logSerializers) { | ||
|  |       loggerOpts.serializers = context.logSerializers | ||
|  |     } | ||
|  |     const childLogger = logger.child(loggerBinding, loggerOpts) | ||
|  |     childLogger[kDisableRequestLogging] = disableRequestLogging | ||
|  | 
 | ||
|  |     const queryPrefix = req.url.indexOf('?') | ||
|  |     const query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '') | ||
|  |     const request = new context.Request(id, params, req, query, childLogger, context) | ||
|  |     const reply = new context.Reply(res, request, childLogger) | ||
|  | 
 | ||
|  |     if (disableRequestLogging === false) { | ||
|  |       childLogger.info({ req: request }, 'incoming request') | ||
|  |     } | ||
|  | 
 | ||
|  |     if (hasLogger === true || context.onResponse !== null) { | ||
|  |       setupResponseListeners(reply) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (context.onRequest !== null) { | ||
|  |       hookRunner( | ||
|  |         context.onRequest, | ||
|  |         hookIterator, | ||
|  |         request, | ||
|  |         reply, | ||
|  |         runPreParsing | ||
|  |       ) | ||
|  |     } else { | ||
|  |       runPreParsing(null, request, reply) | ||
|  |     } | ||
|  | 
 | ||
|  |     if (context.onTimeout !== null) { | ||
|  |       if (!request.raw.socket._meta) { | ||
|  |         request.raw.socket.on('timeout', handleTimeout) | ||
|  |       } | ||
|  |       request.raw.socket._meta = { context, request, reply } | ||
|  |     } | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function handleTimeout () { | ||
|  |   const { context, request, reply } = this._meta | ||
|  |   hookRunner( | ||
|  |     context.onTimeout, | ||
|  |     hookIterator, | ||
|  |     request, | ||
|  |     reply, | ||
|  |     noop | ||
|  |   ) | ||
|  | } | ||
|  | 
 | ||
|  | function validateBodyLimitOption (bodyLimit) { | ||
|  |   if (bodyLimit === undefined) return | ||
|  |   if (!Number.isInteger(bodyLimit) || bodyLimit <= 0) { | ||
|  |     throw new TypeError(`'bodyLimit' option must be an integer > 0. Got '${bodyLimit}'`) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function runPreParsing (err, request, reply) { | ||
|  |   if (reply.sent === true) return | ||
|  |   if (err != null) { | ||
|  |     reply.send(err) | ||
|  |     return | ||
|  |   } | ||
|  | 
 | ||
|  |   request[kRequestPayloadStream] = request.raw | ||
|  | 
 | ||
|  |   if (reply.context.preParsing !== null) { | ||
|  |     preParsingHookRunner(reply.context.preParsing, request, reply, handleRequest) | ||
|  |   } else { | ||
|  |     handleRequest(null, request, reply) | ||
|  |   } | ||
|  | } | ||
|  | 
 | ||
|  | function preParsingHookRunner (functions, request, reply, cb) { | ||
|  |   let i = 0 | ||
|  | 
 | ||
|  |   function next (err, stream) { | ||
|  |     if (reply.sent) { | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     if (typeof stream !== 'undefined') { | ||
|  |       request[kRequestPayloadStream] = stream | ||
|  |     } | ||
|  | 
 | ||
|  |     if (err || i === functions.length) { | ||
|  |       if (err && !(err instanceof Error)) { | ||
|  |         reply[kReplyIsError] = true | ||
|  |       } | ||
|  | 
 | ||
|  |       cb(err, request, reply) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     const fn = functions[i++] | ||
|  |     let result | ||
|  |     try { | ||
|  |       if (fn[kHooksDeprecatedPreParsing]) { | ||
|  |         result = fn(request, reply, next) | ||
|  |       } else { | ||
|  |         result = fn(request, reply, request[kRequestPayloadStream], next) | ||
|  |       } | ||
|  |     } catch (error) { | ||
|  |       next(error) | ||
|  |       return | ||
|  |     } | ||
|  | 
 | ||
|  |     if (result && typeof result.then === 'function') { | ||
|  |       result.then(handleResolve, handleReject) | ||
|  |     } | ||
|  |   } | ||
|  | 
 | ||
|  |   function handleResolve (stream) { | ||
|  |     next(null, stream) | ||
|  |   } | ||
|  | 
 | ||
|  |   function handleReject (err) { | ||
|  |     next(err) | ||
|  |   } | ||
|  | 
 | ||
|  |   next(null, request[kRequestPayloadStream]) | ||
|  | } | ||
|  | 
 | ||
|  | /** | ||
|  |  * Used within the route handler as a `net.Socket.close` event handler. | ||
|  |  * The purpose is to remove a socket from the tracked sockets collection when | ||
|  |  * the socket has naturally timed out. | ||
|  |  */ | ||
|  | function removeTrackedSocket () { | ||
|  |   this.keepAliveConnections.delete(this.socket) | ||
|  | } | ||
|  | 
 | ||
|  | function noop () { } | ||
|  | 
 | ||
|  | module.exports = { buildRouting, validateBodyLimitOption } |