Spaces:
Running
Running
/*! | |
* router | |
* Copyright(c) 2013 Roman Shtylman | |
* Copyright(c) 2014-2022 Douglas Christopher Wilson | |
* MIT Licensed | |
*/ | |
/** | |
* 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) | |
} | |
} | |