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

'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 }