749 lines
16 KiB
JavaScript
749 lines
16 KiB
JavaScript
|
/*!
|
||
|
* router
|
||
|
* Copyright(c) 2013 Roman Shtylman
|
||
|
* Copyright(c) 2014-2022 Douglas Christopher Wilson
|
||
|
* MIT Licensed
|
||
|
*/
|
||
|
|
||
|
'use strict'
|
||
|
|
||
|
/**
|
||
|
* Module dependencies.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
const isPromise = require('is-promise')
|
||
|
const Layer = require('./lib/layer')
|
||
|
const { METHODS } = require('node:http')
|
||
|
const parseUrl = require('parseurl')
|
||
|
const Route = require('./lib/route')
|
||
|
const debug = require('debug')('router')
|
||
|
const deprecate = require('depd')('router')
|
||
|
|
||
|
/**
|
||
|
* Module variables.
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
const slice = Array.prototype.slice
|
||
|
const flatten = Array.prototype.flat
|
||
|
const methods = METHODS.map((method) => method.toLowerCase())
|
||
|
|
||
|
/**
|
||
|
* Expose `Router`.
|
||
|
*/
|
||
|
|
||
|
module.exports = Router
|
||
|
|
||
|
/**
|
||
|
* Expose `Route`.
|
||
|
*/
|
||
|
|
||
|
module.exports.Route = Route
|
||
|
|
||
|
/**
|
||
|
* Initialize a new `Router` with the given `options`.
|
||
|
*
|
||
|
* @param {object} [options]
|
||
|
* @return {Router} which is a callable function
|
||
|
* @public
|
||
|
*/
|
||
|
|
||
|
function Router (options) {
|
||
|
if (!(this instanceof Router)) {
|
||
|
return new Router(options)
|
||
|
}
|
||
|
|
||
|
const opts = options || {}
|
||
|
|
||
|
function router (req, res, next) {
|
||
|
router.handle(req, res, next)
|
||
|
}
|
||
|
|
||
|
// inherit from the correct prototype
|
||
|
Object.setPrototypeOf(router, this)
|
||
|
|
||
|
router.caseSensitive = opts.caseSensitive
|
||
|
router.mergeParams = opts.mergeParams
|
||
|
router.params = {}
|
||
|
router.strict = opts.strict
|
||
|
router.stack = []
|
||
|
|
||
|
return router
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Router prototype inherits from a Function.
|
||
|
*/
|
||
|
|
||
|
/* istanbul ignore next */
|
||
|
Router.prototype = function () {}
|
||
|
|
||
|
/**
|
||
|
* Map the given param placeholder `name`(s) to the given callback.
|
||
|
*
|
||
|
* Parameter mapping is used to provide pre-conditions to routes
|
||
|
* which use normalized placeholders. For example a _:user_id_ parameter
|
||
|
* could automatically load a user's information from the database without
|
||
|
* any additional code.
|
||
|
*
|
||
|
* The callback uses the same signature as middleware, the only difference
|
||
|
* being that the value of the placeholder is passed, in this case the _id_
|
||
|
* of the user. Once the `next()` function is invoked, just like middleware
|
||
|
* it will continue on to execute the route, or subsequent parameter functions.
|
||
|
*
|
||
|
* Just like in middleware, you must either respond to the request or call next
|
||
|
* to avoid stalling the request.
|
||
|
*
|
||
|
* router.param('user_id', function(req, res, next, id){
|
||
|
* User.find(id, function(err, user){
|
||
|
* if (err) {
|
||
|
* return next(err)
|
||
|
* } else if (!user) {
|
||
|
* return next(new Error('failed to load user'))
|
||
|
* }
|
||
|
* req.user = user
|
||
|
* next()
|
||
|
* })
|
||
|
* })
|
||
|
*
|
||
|
* @param {string} name
|
||
|
* @param {function} fn
|
||
|
* @public
|
||
|
*/
|
||
|
|
||
|
Router.prototype.param = function param (name, fn) {
|
||
|
if (!name) {
|
||
|
throw new TypeError('argument name is required')
|
||
|
}
|
||
|
|
||
|
if (typeof name !== 'string') {
|
||
|
throw new TypeError('argument name must be a string')
|
||
|
}
|
||
|
|
||
|
if (!fn) {
|
||
|
throw new TypeError('argument fn is required')
|
||
|
}
|
||
|
|
||
|
if (typeof fn !== 'function') {
|
||
|
throw new TypeError('argument fn must be a function')
|
||
|
}
|
||
|
|
||
|
let params = this.params[name]
|
||
|
|
||
|
if (!params) {
|
||
|
params = this.params[name] = []
|
||
|
}
|
||
|
|
||
|
params.push(fn)
|
||
|
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Dispatch a req, res into the router.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
Router.prototype.handle = function handle (req, res, callback) {
|
||
|
if (!callback) {
|
||
|
throw new TypeError('argument callback is required')
|
||
|
}
|
||
|
|
||
|
debug('dispatching %s %s', req.method, req.url)
|
||
|
|
||
|
let idx = 0
|
||
|
let methods
|
||
|
const protohost = getProtohost(req.url) || ''
|
||
|
let removed = ''
|
||
|
const self = this
|
||
|
let slashAdded = false
|
||
|
let sync = 0
|
||
|
const paramcalled = {}
|
||
|
|
||
|
// middleware and routes
|
||
|
const stack = this.stack
|
||
|
|
||
|
// manage inter-router variables
|
||
|
const parentParams = req.params
|
||
|
const parentUrl = req.baseUrl || ''
|
||
|
let done = restore(callback, req, 'baseUrl', 'next', 'params')
|
||
|
|
||
|
// setup next layer
|
||
|
req.next = next
|
||
|
|
||
|
// for options requests, respond with a default if nothing else responds
|
||
|
if (req.method === 'OPTIONS') {
|
||
|
methods = []
|
||
|
done = wrap(done, generateOptionsResponder(res, methods))
|
||
|
}
|
||
|
|
||
|
// setup basic req values
|
||
|
req.baseUrl = parentUrl
|
||
|
req.originalUrl = req.originalUrl || req.url
|
||
|
|
||
|
next()
|
||
|
|
||
|
function next (err) {
|
||
|
let layerError = err === 'route'
|
||
|
? null
|
||
|
: err
|
||
|
|
||
|
// remove added slash
|
||
|
if (slashAdded) {
|
||
|
req.url = req.url.slice(1)
|
||
|
slashAdded = false
|
||
|
}
|
||
|
|
||
|
// restore altered req.url
|
||
|
if (removed.length !== 0) {
|
||
|
req.baseUrl = parentUrl
|
||
|
req.url = protohost + removed + req.url.slice(protohost.length)
|
||
|
removed = ''
|
||
|
}
|
||
|
|
||
|
// signal to exit router
|
||
|
if (layerError === 'router') {
|
||
|
setImmediate(done, null)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// no more matching layers
|
||
|
if (idx >= stack.length) {
|
||
|
setImmediate(done, layerError)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// max sync stack
|
||
|
if (++sync > 100) {
|
||
|
return setImmediate(next, err)
|
||
|
}
|
||
|
|
||
|
// get pathname of request
|
||
|
const path = getPathname(req)
|
||
|
|
||
|
if (path == null) {
|
||
|
return done(layerError)
|
||
|
}
|
||
|
|
||
|
// find next matching layer
|
||
|
let layer
|
||
|
let match
|
||
|
let route
|
||
|
|
||
|
while (match !== true && idx < stack.length) {
|
||
|
layer = stack[idx++]
|
||
|
match = matchLayer(layer, path)
|
||
|
route = layer.route
|
||
|
|
||
|
if (typeof match !== 'boolean') {
|
||
|
// hold on to layerError
|
||
|
layerError = layerError || match
|
||
|
}
|
||
|
|
||
|
if (match !== true) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if (!route) {
|
||
|
// process non-route handlers normally
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
if (layerError) {
|
||
|
// routes do not match with a pending error
|
||
|
match = false
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
const method = req.method
|
||
|
const hasMethod = route._handlesMethod(method)
|
||
|
|
||
|
// build up automatic options response
|
||
|
if (!hasMethod && method === 'OPTIONS' && methods) {
|
||
|
methods.push.apply(methods, route._methods())
|
||
|
}
|
||
|
|
||
|
// don't even bother matching route
|
||
|
if (!hasMethod && method !== 'HEAD') {
|
||
|
match = false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// no match
|
||
|
if (match !== true) {
|
||
|
return done(layerError)
|
||
|
}
|
||
|
|
||
|
// store route for dispatch on change
|
||
|
if (route) {
|
||
|
req.route = route
|
||
|
}
|
||
|
|
||
|
// Capture one-time layer values
|
||
|
req.params = self.mergeParams
|
||
|
? mergeParams(layer.params, parentParams)
|
||
|
: layer.params
|
||
|
const layerPath = layer.path
|
||
|
|
||
|
// this should be done for the layer
|
||
|
processParams(self.params, layer, paramcalled, req, res, function (err) {
|
||
|
if (err) {
|
||
|
next(layerError || err)
|
||
|
} else if (route) {
|
||
|
layer.handleRequest(req, res, next)
|
||
|
} else {
|
||
|
trimPrefix(layer, layerError, layerPath, path)
|
||
|
}
|
||
|
|
||
|
sync = 0
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function trimPrefix (layer, layerError, layerPath, path) {
|
||
|
if (layerPath.length !== 0) {
|
||
|
// Validate path is a prefix match
|
||
|
if (layerPath !== path.substring(0, layerPath.length)) {
|
||
|
next(layerError)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Validate path breaks on a path separator
|
||
|
const c = path[layerPath.length]
|
||
|
if (c && c !== '/') {
|
||
|
next(layerError)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Trim off the part of the url that matches the route
|
||
|
// middleware (.use stuff) needs to have the path stripped
|
||
|
debug('trim prefix (%s) from url %s', layerPath, req.url)
|
||
|
removed = layerPath
|
||
|
req.url = protohost + req.url.slice(protohost.length + removed.length)
|
||
|
|
||
|
// Ensure leading slash
|
||
|
if (!protohost && req.url[0] !== '/') {
|
||
|
req.url = '/' + req.url
|
||
|
slashAdded = true
|
||
|
}
|
||
|
|
||
|
// Setup base URL (no trailing slash)
|
||
|
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
|
||
|
? removed.substring(0, removed.length - 1)
|
||
|
: removed)
|
||
|
}
|
||
|
|
||
|
debug('%s %s : %s', layer.name, layerPath, req.originalUrl)
|
||
|
|
||
|
if (layerError) {
|
||
|
layer.handleError(layerError, req, res, next)
|
||
|
} else {
|
||
|
layer.handleRequest(req, res, next)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Use the given middleware function, with optional path, defaulting to "/".
|
||
|
*
|
||
|
* Use (like `.all`) will run for any http METHOD, but it will not add
|
||
|
* handlers for those methods so OPTIONS requests will not consider `.use`
|
||
|
* functions even if they could respond.
|
||
|
*
|
||
|
* The other difference is that _route_ path is stripped and not visible
|
||
|
* to the handler function. The main effect of this feature is that mounted
|
||
|
* handlers can operate without any code changes regardless of the "prefix"
|
||
|
* pathname.
|
||
|
*
|
||
|
* @public
|
||
|
*/
|
||
|
|
||
|
Router.prototype.use = function use (handler) {
|
||
|
let offset = 0
|
||
|
let path = '/'
|
||
|
|
||
|
// default path to '/'
|
||
|
// disambiguate router.use([handler])
|
||
|
if (typeof handler !== 'function') {
|
||
|
let arg = handler
|
||
|
|
||
|
while (Array.isArray(arg) && arg.length !== 0) {
|
||
|
arg = arg[0]
|
||
|
}
|
||
|
|
||
|
// first arg is the path
|
||
|
if (typeof arg !== 'function') {
|
||
|
offset = 1
|
||
|
path = handler
|
||
|
}
|
||
|
}
|
||
|
|
||
|
const callbacks = flatten.call(slice.call(arguments, offset), Infinity)
|
||
|
|
||
|
if (callbacks.length === 0) {
|
||
|
throw new TypeError('argument handler is required')
|
||
|
}
|
||
|
|
||
|
for (let i = 0; i < callbacks.length; i++) {
|
||
|
const fn = callbacks[i]
|
||
|
|
||
|
if (typeof fn !== 'function') {
|
||
|
throw new TypeError('argument handler must be a function')
|
||
|
}
|
||
|
|
||
|
// add the middleware
|
||
|
debug('use %o %s', path, fn.name || '<anonymous>')
|
||
|
|
||
|
const layer = new Layer(path, {
|
||
|
sensitive: this.caseSensitive,
|
||
|
strict: false,
|
||
|
end: false
|
||
|
}, fn)
|
||
|
|
||
|
layer.route = undefined
|
||
|
|
||
|
this.stack.push(layer)
|
||
|
}
|
||
|
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Create a new Route for the given path.
|
||
|
*
|
||
|
* Each route contains a separate middleware stack and VERB handlers.
|
||
|
*
|
||
|
* See the Route api documentation for details on adding handlers
|
||
|
* and middleware to routes.
|
||
|
*
|
||
|
* @param {string} path
|
||
|
* @return {Route}
|
||
|
* @public
|
||
|
*/
|
||
|
|
||
|
Router.prototype.route = function route (path) {
|
||
|
const route = new Route(path)
|
||
|
|
||
|
const layer = new Layer(path, {
|
||
|
sensitive: this.caseSensitive,
|
||
|
strict: this.strict,
|
||
|
end: true
|
||
|
}, handle)
|
||
|
|
||
|
function handle (req, res, next) {
|
||
|
route.dispatch(req, res, next)
|
||
|
}
|
||
|
|
||
|
layer.route = route
|
||
|
|
||
|
this.stack.push(layer)
|
||
|
return route
|
||
|
}
|
||
|
|
||
|
// create Router#VERB functions
|
||
|
methods.concat('all').forEach(function (method) {
|
||
|
Router.prototype[method] = function (path) {
|
||
|
const route = this.route(path)
|
||
|
route[method].apply(route, slice.call(arguments, 1))
|
||
|
return this
|
||
|
}
|
||
|
})
|
||
|
|
||
|
/**
|
||
|
* Generate a callback that will make an OPTIONS response.
|
||
|
*
|
||
|
* @param {OutgoingMessage} res
|
||
|
* @param {array} methods
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function generateOptionsResponder (res, methods) {
|
||
|
return function onDone (fn, err) {
|
||
|
if (err || methods.length === 0) {
|
||
|
return fn(err)
|
||
|
}
|
||
|
|
||
|
trySendOptionsResponse(res, methods, fn)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get pathname of request.
|
||
|
*
|
||
|
* @param {IncomingMessage} req
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function getPathname (req) {
|
||
|
try {
|
||
|
return parseUrl(req).pathname
|
||
|
} catch (err) {
|
||
|
return undefined
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Get get protocol + host for a URL.
|
||
|
*
|
||
|
* @param {string} url
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function getProtohost (url) {
|
||
|
if (typeof url !== 'string' || url.length === 0 || url[0] === '/') {
|
||
|
return undefined
|
||
|
}
|
||
|
|
||
|
const searchIndex = url.indexOf('?')
|
||
|
const pathLength = searchIndex !== -1
|
||
|
? searchIndex
|
||
|
: url.length
|
||
|
const fqdnIndex = url.substring(0, pathLength).indexOf('://')
|
||
|
|
||
|
return fqdnIndex !== -1
|
||
|
? url.substring(0, url.indexOf('/', 3 + fqdnIndex))
|
||
|
: undefined
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Match path to a layer.
|
||
|
*
|
||
|
* @param {Layer} layer
|
||
|
* @param {string} path
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function matchLayer (layer, path) {
|
||
|
try {
|
||
|
return layer.match(path)
|
||
|
} catch (err) {
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Merge params with parent params
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function mergeParams (params, parent) {
|
||
|
if (typeof parent !== 'object' || !parent) {
|
||
|
return params
|
||
|
}
|
||
|
|
||
|
// make copy of parent for base
|
||
|
const obj = Object.assign({}, parent)
|
||
|
|
||
|
// simple non-numeric merging
|
||
|
if (!(0 in params) || !(0 in parent)) {
|
||
|
return Object.assign(obj, params)
|
||
|
}
|
||
|
|
||
|
let i = 0
|
||
|
let o = 0
|
||
|
|
||
|
// determine numeric gap in params
|
||
|
while (i in params) {
|
||
|
i++
|
||
|
}
|
||
|
|
||
|
// determine numeric gap in parent
|
||
|
while (o in parent) {
|
||
|
o++
|
||
|
}
|
||
|
|
||
|
// offset numeric indices in params before merge
|
||
|
for (i--; i >= 0; i--) {
|
||
|
params[i + o] = params[i]
|
||
|
|
||
|
// create holes for the merge when necessary
|
||
|
if (i < o) {
|
||
|
delete params[i]
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return Object.assign(obj, params)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Process any parameters for the layer.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function processParams (params, layer, called, req, res, done) {
|
||
|
// captured parameters from the layer, keys and values
|
||
|
const keys = layer.keys
|
||
|
|
||
|
// fast track
|
||
|
if (!keys || keys.length === 0) {
|
||
|
return done()
|
||
|
}
|
||
|
|
||
|
let i = 0
|
||
|
let paramIndex = 0
|
||
|
let key
|
||
|
let paramVal
|
||
|
let paramCallbacks
|
||
|
let paramCalled
|
||
|
|
||
|
// process params in order
|
||
|
// param callbacks can be async
|
||
|
function param (err) {
|
||
|
if (err) {
|
||
|
return done(err)
|
||
|
}
|
||
|
|
||
|
if (i >= keys.length) {
|
||
|
return done()
|
||
|
}
|
||
|
|
||
|
paramIndex = 0
|
||
|
key = keys[i++]
|
||
|
paramVal = req.params[key]
|
||
|
paramCallbacks = params[key]
|
||
|
paramCalled = called[key]
|
||
|
|
||
|
if (paramVal === undefined || !paramCallbacks) {
|
||
|
return param()
|
||
|
}
|
||
|
|
||
|
// param previously called with same value or error occurred
|
||
|
if (paramCalled && (paramCalled.match === paramVal ||
|
||
|
(paramCalled.error && paramCalled.error !== 'route'))) {
|
||
|
// restore value
|
||
|
req.params[key] = paramCalled.value
|
||
|
|
||
|
// next param
|
||
|
return param(paramCalled.error)
|
||
|
}
|
||
|
|
||
|
called[key] = paramCalled = {
|
||
|
error: null,
|
||
|
match: paramVal,
|
||
|
value: paramVal
|
||
|
}
|
||
|
|
||
|
paramCallback()
|
||
|
}
|
||
|
|
||
|
// single param callbacks
|
||
|
function paramCallback (err) {
|
||
|
const fn = paramCallbacks[paramIndex++]
|
||
|
|
||
|
// store updated value
|
||
|
paramCalled.value = req.params[key]
|
||
|
|
||
|
if (err) {
|
||
|
// store error
|
||
|
paramCalled.error = err
|
||
|
param(err)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
if (!fn) return param()
|
||
|
|
||
|
try {
|
||
|
const ret = fn(req, res, paramCallback, paramVal, key)
|
||
|
if (isPromise(ret)) {
|
||
|
if (!(ret instanceof Promise)) {
|
||
|
deprecate('parameters that are Promise-like are deprecated, use a native Promise instead')
|
||
|
}
|
||
|
|
||
|
ret.then(null, function (error) {
|
||
|
paramCallback(error || new Error('Rejected promise'))
|
||
|
})
|
||
|
}
|
||
|
} catch (e) {
|
||
|
paramCallback(e)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
param()
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Restore obj props after function
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function restore (fn, obj) {
|
||
|
const props = new Array(arguments.length - 2)
|
||
|
const vals = new Array(arguments.length - 2)
|
||
|
|
||
|
for (let i = 0; i < props.length; i++) {
|
||
|
props[i] = arguments[i + 2]
|
||
|
vals[i] = obj[props[i]]
|
||
|
}
|
||
|
|
||
|
return function () {
|
||
|
// restore vals
|
||
|
for (let i = 0; i < props.length; i++) {
|
||
|
obj[props[i]] = vals[i]
|
||
|
}
|
||
|
|
||
|
return fn.apply(this, arguments)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Send an OPTIONS response.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function sendOptionsResponse (res, methods) {
|
||
|
const options = Object.create(null)
|
||
|
|
||
|
// build unique method map
|
||
|
for (let i = 0; i < methods.length; i++) {
|
||
|
options[methods[i]] = true
|
||
|
}
|
||
|
|
||
|
// construct the allow list
|
||
|
const allow = Object.keys(options).sort().join(', ')
|
||
|
|
||
|
// send response
|
||
|
res.setHeader('Allow', allow)
|
||
|
res.setHeader('Content-Length', Buffer.byteLength(allow))
|
||
|
res.setHeader('Content-Type', 'text/plain')
|
||
|
res.setHeader('X-Content-Type-Options', 'nosniff')
|
||
|
res.end(allow)
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Try to send an OPTIONS response.
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function trySendOptionsResponse (res, methods, next) {
|
||
|
try {
|
||
|
sendOptionsResponse(res, methods)
|
||
|
} catch (err) {
|
||
|
next(err)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Wrap a function
|
||
|
*
|
||
|
* @private
|
||
|
*/
|
||
|
|
||
|
function wrap (old, fn) {
|
||
|
return function proxy () {
|
||
|
const args = new Array(arguments.length + 1)
|
||
|
|
||
|
args[0] = old
|
||
|
for (let i = 0, len = arguments.length; i < len; i++) {
|
||
|
args[i + 1] = arguments[i]
|
||
|
}
|
||
|
|
||
|
fn.apply(this, args)
|
||
|
}
|
||
|
}
|