diff options
Diffstat (limited to '')
| -rw-r--r-- | node_modules/router/index.js | 748 |
1 files changed, 748 insertions, 0 deletions
diff --git a/node_modules/router/index.js b/node_modules/router/index.js new file mode 100644 index 0000000..4358aeb --- /dev/null +++ b/node_modules/router/index.js @@ -0,0 +1,748 @@ +/*! + * 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) + } +} |