123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748 |
- /*!
- * 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)
- }
- }
|