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.
407 lines
12 KiB
407 lines
12 KiB
'use strict'
|
|
|
|
const FindMyWay = require('find-my-way')
|
|
const proxyAddr = require('proxy-addr')
|
|
const Context = require('./context')
|
|
const { buildMiddie, onRunMiddlewares } = require('./middleware')
|
|
const { hookRunner, hookIterator } = require('./hooks')
|
|
const supportedMethods = ['DELETE', 'GET', 'HEAD', 'PATCH', 'POST', 'PUT', 'OPTIONS']
|
|
const supportedHooks = ['preParsing', 'preValidation', 'onRequest', 'preHandler', 'preSerialization', 'onResponse', 'onSend']
|
|
const validation = require('./validation')
|
|
const buildSchema = validation.build
|
|
const { buildSchemaCompiler } = validation
|
|
const { beforeHandlerWarning } = require('./warnings')
|
|
|
|
const {
|
|
codes: {
|
|
FST_ERR_SCH_BUILD,
|
|
FST_ERR_SCH_MISSING_COMPILER
|
|
}
|
|
} = require('./errors')
|
|
|
|
const {
|
|
kRoutePrefix,
|
|
kLogLevel,
|
|
kLogSerializers,
|
|
kHooks,
|
|
kSchemas,
|
|
kOptions,
|
|
kSchemaCompiler,
|
|
kSchemaResolver,
|
|
kContentTypeParser,
|
|
kReply,
|
|
kReplySerializerDefault,
|
|
kRequest,
|
|
kMiddlewares,
|
|
kGlobalHooks,
|
|
kDisableRequestLogging
|
|
} = require('./symbols.js')
|
|
|
|
function buildRouting (options) {
|
|
const router = FindMyWay(options.config)
|
|
|
|
const schemaCache = new Map()
|
|
schemaCache.put = schemaCache.set
|
|
|
|
let avvio
|
|
let fourOhFour
|
|
let trustProxy
|
|
let requestIdHeader
|
|
let querystringParser
|
|
let requestIdLogLabel
|
|
let logger
|
|
let hasLogger
|
|
let setupResponseListeners
|
|
let throwIfAlreadyStarted
|
|
let proxyFn
|
|
let modifyCoreObjects
|
|
let genReqId
|
|
let disableRequestLogging
|
|
let ignoreTrailingSlash
|
|
let return503OnClosing
|
|
|
|
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
|
|
|
|
proxyFn = getTrustProxyFn(options)
|
|
trustProxy = options.trustProxy
|
|
requestIdHeader = options.requestIdHeader
|
|
querystringParser = options.querystringParser
|
|
requestIdLogLabel = options.requestIdLogLabel
|
|
modifyCoreObjects = options.modifyCoreObjects
|
|
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,
|
|
routeHandler,
|
|
closeRoutes: () => { closing = true },
|
|
printRoutes: router.prettyPrint.bind(router)
|
|
}
|
|
|
|
// Convert shorthand to extended route declaration
|
|
function prepareRoute (method, url, options, handler) {
|
|
if (!handler && typeof options === 'function') {
|
|
handler = options
|
|
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,
|
|
handler: handler || (options && options.handler)
|
|
})
|
|
|
|
return route.call(this, options)
|
|
}
|
|
|
|
// Route management
|
|
function route (opts) {
|
|
throwIfAlreadyStarted('Cannot add route when fastify instance is already started!')
|
|
|
|
if (Array.isArray(opts.method)) {
|
|
for (var i = 0; i < opts.method.length; i++) {
|
|
if (supportedMethods.indexOf(opts.method[i]) === -1) {
|
|
throw new Error(`${opts.method[i]} method is not supported!`)
|
|
}
|
|
}
|
|
} else {
|
|
if (supportedMethods.indexOf(opts.method) === -1) {
|
|
throw new Error(`${opts.method} method is not supported!`)
|
|
}
|
|
}
|
|
|
|
if (!opts.handler) {
|
|
throw new Error(`Missing handler function for ${opts.method}:${opts.url} route.`)
|
|
}
|
|
|
|
validateBodyLimitOption(opts.bodyLimit)
|
|
|
|
if (opts.preHandler == null && opts.beforeHandler != null) {
|
|
beforeHandlerWarning()
|
|
opts.preHandler = opts.beforeHandler
|
|
}
|
|
|
|
const prefix = this[kRoutePrefix]
|
|
|
|
this.after((notHandledErr, done) => {
|
|
var path = opts.url || opts.path
|
|
if (path === '/' && prefix.length > 0) {
|
|
switch (opts.prefixTrailingSlash) {
|
|
case 'slash':
|
|
afterRouteAdded.call(this, path, notHandledErr, done)
|
|
break
|
|
case 'no-slash':
|
|
afterRouteAdded.call(this, '', notHandledErr, done)
|
|
break
|
|
case 'both':
|
|
default:
|
|
afterRouteAdded.call(this, '', 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, notHandledErr, done)
|
|
}
|
|
}
|
|
} else if (path[0] === '/' && prefix.endsWith('/')) {
|
|
// Ensure that '/prefix/' + '/route' gets registered as '/prefix/route'
|
|
afterRouteAdded.call(this, path.slice(1), notHandledErr, done)
|
|
} else {
|
|
afterRouteAdded.call(this, path, notHandledErr, done)
|
|
}
|
|
})
|
|
|
|
// chainable api
|
|
return this
|
|
|
|
function afterRouteAdded (path, notHandledErr, done) {
|
|
const url = prefix + path
|
|
|
|
opts.url = url
|
|
opts.path = url
|
|
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
|
|
}
|
|
|
|
// run 'onRoute' hooks
|
|
for (const hook of this[kGlobalHooks].onRoute) {
|
|
try {
|
|
hook.call(this, opts)
|
|
} catch (error) {
|
|
done(error)
|
|
return
|
|
}
|
|
}
|
|
|
|
const config = opts.config || {}
|
|
config.url = url
|
|
|
|
const context = new Context(
|
|
opts.schema,
|
|
opts.handler.bind(this),
|
|
this[kReply],
|
|
this[kRequest],
|
|
this[kContentTypeParser],
|
|
config,
|
|
this._errorHandler,
|
|
opts.bodyLimit,
|
|
opts.logLevel,
|
|
opts.logSerializers,
|
|
opts.attachValidation,
|
|
this[kReplySerializerDefault]
|
|
)
|
|
|
|
for (const hook of supportedHooks) {
|
|
if (opts[hook]) {
|
|
if (Array.isArray(opts[hook])) {
|
|
opts[hook] = opts[hook].map(fn => fn.bind(this))
|
|
} else {
|
|
opts[hook] = opts[hook].bind(this)
|
|
}
|
|
}
|
|
}
|
|
|
|
try {
|
|
router.on(opts.method, opts.url, { version: opts.version }, routeHandler, context)
|
|
} catch (err) {
|
|
done(err)
|
|
return
|
|
}
|
|
|
|
// It can happen that a user register a plugin with some hooks/middlewares *after*
|
|
// the route registration. To be sure to load also that hooks/middlewares,
|
|
// we must listen for the avvio's preReady event, and update the context object accordingly.
|
|
avvio.once('preReady', () => {
|
|
const onResponse = this[kHooks].onResponse
|
|
const onSend = this[kHooks].onSend
|
|
const onError = this[kHooks].onError
|
|
|
|
context.onSend = onSend.length ? onSend : null
|
|
context.onError = onError.length ? onError : null
|
|
context.onResponse = onResponse.length ? onResponse : null
|
|
|
|
for (const hook of supportedHooks) {
|
|
const toSet = this[kHooks][hook].concat(opts[hook] || [])
|
|
context[hook] = toSet.length ? toSet : null
|
|
}
|
|
|
|
context._middie = buildMiddie(this[kMiddlewares])
|
|
|
|
// 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) {
|
|
if (this[kSchemaCompiler] == null && this[kSchemaResolver]) {
|
|
throw new FST_ERR_SCH_MISSING_COMPILER(opts.method, url)
|
|
}
|
|
|
|
try {
|
|
if (opts.schemaCompiler == null && this[kSchemaCompiler] == null) {
|
|
const externalSchemas = this[kSchemas].getJsonSchemas({ onlyAbsoluteUri: true })
|
|
this.setSchemaCompiler(buildSchemaCompiler(externalSchemas, this[kOptions].ajv, schemaCache))
|
|
}
|
|
|
|
buildSchema(context, opts.schemaCompiler || this[kSchemaCompiler], this[kSchemas], this[kSchemaResolver])
|
|
} catch (error) {
|
|
// bubble up the FastifyError instance
|
|
throw (error.code ? error : new FST_ERR_SCH_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) {
|
|
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
|
|
}
|
|
}
|
|
|
|
req.id = req.headers[requestIdHeader] || genReqId(req)
|
|
req.originalUrl = req.url
|
|
var hostname = req.headers.host || req.headers[':authority']
|
|
var ip = req.connection.remoteAddress
|
|
var ips
|
|
|
|
if (trustProxy) {
|
|
ip = proxyAddr(req, proxyFn)
|
|
ips = proxyAddr.all(req, proxyFn)
|
|
if (ip !== undefined && req.headers['x-forwarded-host']) {
|
|
hostname = req.headers['x-forwarded-host']
|
|
}
|
|
}
|
|
|
|
var loggerOpts = {
|
|
[requestIdLogLabel]: req.id,
|
|
level: context.logLevel
|
|
}
|
|
|
|
if (context.logSerializers) {
|
|
loggerOpts.serializers = context.logSerializers
|
|
}
|
|
var childLogger = logger.child(loggerOpts)
|
|
childLogger[kDisableRequestLogging] = disableRequestLogging
|
|
|
|
// added hostname, ip, and ips back to the Node req object to maintain backward compatibility
|
|
if (modifyCoreObjects) {
|
|
req.hostname = hostname
|
|
req.ip = ip
|
|
req.ips = ips
|
|
|
|
req.log = res.log = childLogger
|
|
}
|
|
|
|
if (disableRequestLogging === false) {
|
|
childLogger.info({ req }, 'incoming request')
|
|
}
|
|
|
|
var queryPrefix = req.url.indexOf('?')
|
|
var query = querystringParser(queryPrefix > -1 ? req.url.slice(queryPrefix + 1) : '')
|
|
var request = new context.Request(params, req, query, req.headers, childLogger, ip, ips, hostname)
|
|
var reply = new context.Reply(res, context, request, childLogger)
|
|
|
|
if (hasLogger === true || context.onResponse !== null) {
|
|
setupResponseListeners(reply)
|
|
}
|
|
|
|
if (context.onRequest !== null) {
|
|
hookRunner(
|
|
context.onRequest,
|
|
hookIterator,
|
|
request,
|
|
reply,
|
|
middlewareCallback
|
|
)
|
|
} else {
|
|
middlewareCallback(null, request, reply)
|
|
}
|
|
}
|
|
}
|
|
|
|
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 middlewareCallback (err, request, reply) {
|
|
if (reply.sent === true) return
|
|
if (err != null) {
|
|
reply.send(err)
|
|
return
|
|
}
|
|
|
|
if (reply.context._middie !== null) {
|
|
reply.context._middie.run(request.raw, reply.res, reply)
|
|
} else {
|
|
onRunMiddlewares(null, null, null, reply)
|
|
}
|
|
}
|
|
|
|
function getTrustProxyFn (options) {
|
|
const tp = options.trustProxy
|
|
if (typeof tp === 'function') {
|
|
return tp
|
|
}
|
|
if (tp === true) {
|
|
// Support plain true/false
|
|
return function () { return true }
|
|
}
|
|
if (typeof tp === 'number') {
|
|
// Support trusting hop count
|
|
return function (a, i) { return i < tp }
|
|
}
|
|
if (typeof tp === 'string') {
|
|
// Support comma-separated tps
|
|
const vals = tp.split(',').map(it => it.trim())
|
|
return proxyAddr.compile(vals)
|
|
}
|
|
return proxyAddr.compile(tp || [])
|
|
}
|
|
|
|
module.exports = { buildRouting, validateBodyLimitOption }
|