@pirxpilot/router 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/HISTORY.md +206 -0
- package/LICENSE +23 -0
- package/README.md +17 -0
- package/SECURITY.md +24 -0
- package/index.js +717 -0
- package/lib/layer.js +225 -0
- package/lib/route.js +235 -0
- package/package.json +39 -0
package/index.js
ADDED
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
/*!
|
|
2
|
+
* router
|
|
3
|
+
* Copyright(c) 2013 Roman Shtylman
|
|
4
|
+
* Copyright(c) 2014-2022 Douglas Christopher Wilson
|
|
5
|
+
* MIT Licensed
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Module dependencies.
|
|
12
|
+
* @private
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const Layer = require('./lib/layer')
|
|
16
|
+
const methods = require('methods')
|
|
17
|
+
const parseUrl = require('parseurl')
|
|
18
|
+
const Route = require('./lib/route')
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Module variables.
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Expose `Router`.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
module.exports = Router
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Expose `Route`.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
module.exports.Route = Route
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Initialize a new `Router` with the given `options`.
|
|
39
|
+
*
|
|
40
|
+
* @param {object} [options]
|
|
41
|
+
* @return {Router} which is a callable function
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
function Router (options) {
|
|
46
|
+
if (!(this instanceof Router)) {
|
|
47
|
+
return new Router(options)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const opts = options || {}
|
|
51
|
+
|
|
52
|
+
function router (req, res, next) {
|
|
53
|
+
router.handle(req, res, next)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// inherit from the correct prototype
|
|
57
|
+
Object.setPrototypeOf(router, this)
|
|
58
|
+
|
|
59
|
+
router.caseSensitive = opts.caseSensitive
|
|
60
|
+
router.mergeParams = opts.mergeParams
|
|
61
|
+
router.params = {}
|
|
62
|
+
router.strict = opts.strict
|
|
63
|
+
router.stack = []
|
|
64
|
+
|
|
65
|
+
return router
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Router prototype inherits from a Function.
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
/* istanbul ignore next */
|
|
73
|
+
Router.prototype = function () {}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Map the given param placeholder `name`(s) to the given callback.
|
|
77
|
+
*
|
|
78
|
+
* Parameter mapping is used to provide pre-conditions to routes
|
|
79
|
+
* which use normalized placeholders. For example a _:user_id_ parameter
|
|
80
|
+
* could automatically load a user's information from the database without
|
|
81
|
+
* any additional code.
|
|
82
|
+
*
|
|
83
|
+
* The callback uses the same signature as middleware, the only difference
|
|
84
|
+
* being that the value of the placeholder is passed, in this case the _id_
|
|
85
|
+
* of the user. Once the `next()` function is invoked, just like middleware
|
|
86
|
+
* it will continue on to execute the route, or subsequent parameter functions.
|
|
87
|
+
*
|
|
88
|
+
* Just like in middleware, you must either respond to the request or call next
|
|
89
|
+
* to avoid stalling the request.
|
|
90
|
+
*
|
|
91
|
+
* router.param('user_id', function(req, res, next, id){
|
|
92
|
+
* User.find(id, function(err, user){
|
|
93
|
+
* if (err) {
|
|
94
|
+
* return next(err)
|
|
95
|
+
* } else if (!user) {
|
|
96
|
+
* return next(new Error('failed to load user'))
|
|
97
|
+
* }
|
|
98
|
+
* req.user = user
|
|
99
|
+
* next()
|
|
100
|
+
* })
|
|
101
|
+
* })
|
|
102
|
+
*
|
|
103
|
+
* @param {string} name
|
|
104
|
+
* @param {function} fn
|
|
105
|
+
* @public
|
|
106
|
+
*/
|
|
107
|
+
|
|
108
|
+
Router.prototype.param = function param (name, fn) {
|
|
109
|
+
if (!name) {
|
|
110
|
+
throw new TypeError('argument name is required')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (typeof name !== 'string') {
|
|
114
|
+
throw new TypeError('argument name must be a string')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!fn) {
|
|
118
|
+
throw new TypeError('argument fn is required')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof fn !== 'function') {
|
|
122
|
+
throw new TypeError('argument fn must be a function')
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let params = this.params[name]
|
|
126
|
+
|
|
127
|
+
if (!params) {
|
|
128
|
+
params = this.params[name] = []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
params.push(fn)
|
|
132
|
+
|
|
133
|
+
return this
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Dispatch a req, res into the router.
|
|
138
|
+
*
|
|
139
|
+
* @private
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
Router.prototype.handle = function handle (req, res, callback) {
|
|
143
|
+
if (!callback) {
|
|
144
|
+
throw new TypeError('argument callback is required')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let idx = 0
|
|
148
|
+
let methods
|
|
149
|
+
const protohost = getProtohost(req.url) || ''
|
|
150
|
+
let removed = ''
|
|
151
|
+
const self = this
|
|
152
|
+
let slashAdded = false
|
|
153
|
+
let sync = 0
|
|
154
|
+
const paramcalled = {}
|
|
155
|
+
|
|
156
|
+
// middleware and routes
|
|
157
|
+
const stack = this.stack
|
|
158
|
+
|
|
159
|
+
// manage inter-router variables
|
|
160
|
+
const parentParams = req.params
|
|
161
|
+
const parentUrl = req.baseUrl || ''
|
|
162
|
+
let done = restore(callback, req, 'baseUrl', 'next', 'params')
|
|
163
|
+
|
|
164
|
+
// setup next layer
|
|
165
|
+
req.next = next
|
|
166
|
+
|
|
167
|
+
// for options requests, respond with a default if nothing else responds
|
|
168
|
+
if (req.method === 'OPTIONS') {
|
|
169
|
+
methods = []
|
|
170
|
+
done = wrap(done, generateOptionsResponder(res, methods))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// setup basic req values
|
|
174
|
+
req.baseUrl = parentUrl
|
|
175
|
+
req.originalUrl = req.originalUrl || req.url
|
|
176
|
+
|
|
177
|
+
next()
|
|
178
|
+
|
|
179
|
+
function next (err) {
|
|
180
|
+
let layerError = err === 'route'
|
|
181
|
+
? null
|
|
182
|
+
: err
|
|
183
|
+
|
|
184
|
+
// remove added slash
|
|
185
|
+
if (slashAdded) {
|
|
186
|
+
req.url = req.url.slice(1)
|
|
187
|
+
slashAdded = false
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// restore altered req.url
|
|
191
|
+
if (removed.length !== 0) {
|
|
192
|
+
req.baseUrl = parentUrl
|
|
193
|
+
req.url = protohost + removed + req.url.slice(protohost.length)
|
|
194
|
+
removed = ''
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// signal to exit router
|
|
198
|
+
if (layerError === 'router') {
|
|
199
|
+
setImmediate(done, null)
|
|
200
|
+
return
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// no more matching layers
|
|
204
|
+
if (idx >= stack.length) {
|
|
205
|
+
setImmediate(done, layerError)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// max sync stack
|
|
210
|
+
if (++sync > 100) {
|
|
211
|
+
return setImmediate(next, err)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// get pathname of request
|
|
215
|
+
const path = getPathname(req)
|
|
216
|
+
|
|
217
|
+
if (path == null) {
|
|
218
|
+
return done(layerError)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// find next matching layer
|
|
222
|
+
let layer
|
|
223
|
+
let match
|
|
224
|
+
let route
|
|
225
|
+
|
|
226
|
+
while (match !== true && idx < stack.length) {
|
|
227
|
+
layer = stack[idx++]
|
|
228
|
+
match = matchLayer(layer, path)
|
|
229
|
+
route = layer.route
|
|
230
|
+
|
|
231
|
+
if (typeof match !== 'boolean') {
|
|
232
|
+
// hold on to layerError
|
|
233
|
+
layerError = layerError || match
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (match !== true) {
|
|
237
|
+
continue
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (!route) {
|
|
241
|
+
// process non-route handlers normally
|
|
242
|
+
continue
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (layerError) {
|
|
246
|
+
// routes do not match with a pending error
|
|
247
|
+
match = false
|
|
248
|
+
continue
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const method = req.method
|
|
252
|
+
const hasMethod = route._handlesMethod(method)
|
|
253
|
+
|
|
254
|
+
// build up automatic options response
|
|
255
|
+
if (!hasMethod && method === 'OPTIONS' && methods) {
|
|
256
|
+
methods.push.apply(methods, route._methods())
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// don't even bother matching route
|
|
260
|
+
if (!hasMethod && method !== 'HEAD') {
|
|
261
|
+
match = false
|
|
262
|
+
continue
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// no match
|
|
267
|
+
if (match !== true) {
|
|
268
|
+
return done(layerError)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// store route for dispatch on change
|
|
272
|
+
if (route) {
|
|
273
|
+
req.route = route
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Capture one-time layer values
|
|
277
|
+
req.params = self.mergeParams
|
|
278
|
+
? mergeParams(layer.params, parentParams)
|
|
279
|
+
: layer.params
|
|
280
|
+
const layerPath = layer.path
|
|
281
|
+
|
|
282
|
+
// this should be done for the layer
|
|
283
|
+
processParams(self.params, layer, paramcalled, req, res, function (err) {
|
|
284
|
+
if (err) {
|
|
285
|
+
next(layerError || err)
|
|
286
|
+
} else if (route) {
|
|
287
|
+
layer.handleRequest(req, res, next)
|
|
288
|
+
} else {
|
|
289
|
+
trimPrefix(layer, layerError, layerPath, path)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
sync = 0
|
|
293
|
+
})
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function trimPrefix (layer, layerError, layerPath, path) {
|
|
297
|
+
if (layerPath.length !== 0) {
|
|
298
|
+
// Validate path is a prefix match
|
|
299
|
+
if (layerPath !== path.substring(0, layerPath.length)) {
|
|
300
|
+
next(layerError)
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Validate path breaks on a path separator
|
|
305
|
+
const c = path[layerPath.length]
|
|
306
|
+
if (c && c !== '/') {
|
|
307
|
+
next(layerError)
|
|
308
|
+
return
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Trim off the part of the url that matches the route
|
|
312
|
+
// middleware (.use stuff) needs to have the path stripped
|
|
313
|
+
removed = layerPath
|
|
314
|
+
req.url = protohost + req.url.slice(protohost.length + removed.length)
|
|
315
|
+
|
|
316
|
+
// Ensure leading slash
|
|
317
|
+
if (!protohost && req.url[0] !== '/') {
|
|
318
|
+
req.url = '/' + req.url
|
|
319
|
+
slashAdded = true
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Setup base URL (no trailing slash)
|
|
323
|
+
req.baseUrl = parentUrl + (removed[removed.length - 1] === '/'
|
|
324
|
+
? removed.substring(0, removed.length - 1)
|
|
325
|
+
: removed)
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (layerError) {
|
|
329
|
+
layer.handleError(layerError, req, res, next)
|
|
330
|
+
} else {
|
|
331
|
+
layer.handleRequest(req, res, next)
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Use the given middleware function, with optional path, defaulting to "/".
|
|
338
|
+
*
|
|
339
|
+
* Use (like `.all`) will run for any http METHOD, but it will not add
|
|
340
|
+
* handlers for those methods so OPTIONS requests will not consider `.use`
|
|
341
|
+
* functions even if they could respond.
|
|
342
|
+
*
|
|
343
|
+
* The other difference is that _route_ path is stripped and not visible
|
|
344
|
+
* to the handler function. The main effect of this feature is that mounted
|
|
345
|
+
* handlers can operate without any code changes regardless of the "prefix"
|
|
346
|
+
* pathname.
|
|
347
|
+
*
|
|
348
|
+
* @public
|
|
349
|
+
*/
|
|
350
|
+
|
|
351
|
+
Router.prototype.use = function use (handler, ...args) {
|
|
352
|
+
let path = '/'
|
|
353
|
+
|
|
354
|
+
// default path to '/'
|
|
355
|
+
// disambiguate router.use([handler])
|
|
356
|
+
if (typeof handler !== 'function') {
|
|
357
|
+
let arg = handler
|
|
358
|
+
|
|
359
|
+
while (Array.isArray(arg) && arg.length !== 0) {
|
|
360
|
+
arg = arg[0]
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// first arg is the path
|
|
364
|
+
if (typeof arg !== 'function') {
|
|
365
|
+
path = handler
|
|
366
|
+
handler = undefined
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (handler !== undefined) {
|
|
370
|
+
args.unshift(handler)
|
|
371
|
+
}
|
|
372
|
+
const callbacks = args.flat(Infinity)
|
|
373
|
+
|
|
374
|
+
if (callbacks.length === 0) {
|
|
375
|
+
throw new TypeError('argument handler is required')
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (let i = 0; i < callbacks.length; i++) {
|
|
379
|
+
const fn = callbacks[i]
|
|
380
|
+
|
|
381
|
+
if (typeof fn !== 'function') {
|
|
382
|
+
throw new TypeError('argument handler must be a function')
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// add the middleware
|
|
386
|
+
const layer = new Layer(path, {
|
|
387
|
+
sensitive: this.caseSensitive,
|
|
388
|
+
strict: false,
|
|
389
|
+
end: false
|
|
390
|
+
}, fn)
|
|
391
|
+
|
|
392
|
+
layer.route = undefined
|
|
393
|
+
|
|
394
|
+
this.stack.push(layer)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return this
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Create a new Route for the given path.
|
|
402
|
+
*
|
|
403
|
+
* Each route contains a separate middleware stack and VERB handlers.
|
|
404
|
+
*
|
|
405
|
+
* See the Route api documentation for details on adding handlers
|
|
406
|
+
* and middleware to routes.
|
|
407
|
+
*
|
|
408
|
+
* @param {string} path
|
|
409
|
+
* @return {Route}
|
|
410
|
+
* @public
|
|
411
|
+
*/
|
|
412
|
+
|
|
413
|
+
Router.prototype.route = function route (path) {
|
|
414
|
+
const route = new Route(path)
|
|
415
|
+
|
|
416
|
+
const layer = new Layer(path, {
|
|
417
|
+
sensitive: this.caseSensitive,
|
|
418
|
+
strict: this.strict,
|
|
419
|
+
end: true
|
|
420
|
+
}, handle)
|
|
421
|
+
|
|
422
|
+
function handle (req, res, next) {
|
|
423
|
+
route.dispatch(req, res, next)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
layer.route = route
|
|
427
|
+
|
|
428
|
+
this.stack.push(layer)
|
|
429
|
+
return route
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// create Router#VERB functions
|
|
433
|
+
methods.concat('all').forEach(function (method) {
|
|
434
|
+
Router.prototype[method] = function (path, ...args) {
|
|
435
|
+
const route = this.route(path)
|
|
436
|
+
route[method].apply(route, args)
|
|
437
|
+
return this
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Generate a callback that will make an OPTIONS response.
|
|
443
|
+
*
|
|
444
|
+
* @param {OutgoingMessage} res
|
|
445
|
+
* @param {array} methods
|
|
446
|
+
* @private
|
|
447
|
+
*/
|
|
448
|
+
|
|
449
|
+
function generateOptionsResponder (res, methods) {
|
|
450
|
+
return function onDone (fn, err) {
|
|
451
|
+
if (err || methods.length === 0) {
|
|
452
|
+
return fn(err)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
trySendOptionsResponse(res, methods, fn)
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Get pathname of request.
|
|
461
|
+
*
|
|
462
|
+
* @param {IncomingMessage} req
|
|
463
|
+
* @private
|
|
464
|
+
*/
|
|
465
|
+
|
|
466
|
+
function getPathname (req) {
|
|
467
|
+
try {
|
|
468
|
+
return parseUrl(req).pathname
|
|
469
|
+
} catch (err) {
|
|
470
|
+
return undefined
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Get get protocol + host for a URL.
|
|
476
|
+
*
|
|
477
|
+
* @param {string} url
|
|
478
|
+
* @private
|
|
479
|
+
*/
|
|
480
|
+
|
|
481
|
+
function getProtohost (url) {
|
|
482
|
+
if (typeof url !== 'string' || url.length === 0 || url[0] === '/') {
|
|
483
|
+
return undefined
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const searchIndex = url.indexOf('?')
|
|
487
|
+
const pathLength = searchIndex !== -1
|
|
488
|
+
? searchIndex
|
|
489
|
+
: url.length
|
|
490
|
+
const fqdnIndex = url.substring(0, pathLength).indexOf('://')
|
|
491
|
+
|
|
492
|
+
return fqdnIndex !== -1
|
|
493
|
+
? url.substring(0, url.indexOf('/', 3 + fqdnIndex))
|
|
494
|
+
: undefined
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Match path to a layer.
|
|
499
|
+
*
|
|
500
|
+
* @param {Layer} layer
|
|
501
|
+
* @param {string} path
|
|
502
|
+
* @private
|
|
503
|
+
*/
|
|
504
|
+
|
|
505
|
+
function matchLayer (layer, path) {
|
|
506
|
+
try {
|
|
507
|
+
return layer.match(path)
|
|
508
|
+
} catch (err) {
|
|
509
|
+
return err
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Merge params with parent params
|
|
515
|
+
*
|
|
516
|
+
* @private
|
|
517
|
+
*/
|
|
518
|
+
|
|
519
|
+
function mergeParams (params, parent) {
|
|
520
|
+
if (typeof parent !== 'object' || !parent) {
|
|
521
|
+
return params
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
// make copy of parent for base
|
|
525
|
+
const obj = { ...parent }
|
|
526
|
+
|
|
527
|
+
// simple non-numeric merging
|
|
528
|
+
if (!(0 in params) || !(0 in parent)) {
|
|
529
|
+
return Object.assign(obj, params)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
let i = 0
|
|
533
|
+
let o = 0
|
|
534
|
+
|
|
535
|
+
// determine numeric gap in params
|
|
536
|
+
while (i in params) {
|
|
537
|
+
i++
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// determine numeric gap in parent
|
|
541
|
+
while (o in parent) {
|
|
542
|
+
o++
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// offset numeric indices in params before merge
|
|
546
|
+
for (i--; i >= 0; i--) {
|
|
547
|
+
params[i + o] = params[i]
|
|
548
|
+
|
|
549
|
+
// create holes for the merge when necessary
|
|
550
|
+
if (i < o) {
|
|
551
|
+
delete params[i]
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
return Object.assign(obj, params)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Process any parameters for the layer.
|
|
560
|
+
*
|
|
561
|
+
* @private
|
|
562
|
+
*/
|
|
563
|
+
|
|
564
|
+
function processParams (params, layer, called, req, res, done) {
|
|
565
|
+
// captured parameters from the layer, keys and values
|
|
566
|
+
const keys = layer.keys
|
|
567
|
+
|
|
568
|
+
// fast track
|
|
569
|
+
if (!keys || keys.length === 0) {
|
|
570
|
+
return done()
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
let i = 0
|
|
574
|
+
let paramIndex = 0
|
|
575
|
+
let key
|
|
576
|
+
let paramVal
|
|
577
|
+
let paramCallbacks
|
|
578
|
+
let paramCalled
|
|
579
|
+
|
|
580
|
+
// process params in order
|
|
581
|
+
// param callbacks can be async
|
|
582
|
+
function param (err) {
|
|
583
|
+
if (err) {
|
|
584
|
+
return done(err)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
if (i >= keys.length) {
|
|
588
|
+
return done()
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
paramIndex = 0
|
|
592
|
+
key = keys[i++]
|
|
593
|
+
paramVal = req.params[key]
|
|
594
|
+
paramCallbacks = params[key]
|
|
595
|
+
paramCalled = called[key]
|
|
596
|
+
|
|
597
|
+
if (paramVal === undefined || !paramCallbacks) {
|
|
598
|
+
return param()
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// param previously called with same value or error occurred
|
|
602
|
+
if (paramCalled && (paramCalled.match === paramVal ||
|
|
603
|
+
(paramCalled.error && paramCalled.error !== 'route'))) {
|
|
604
|
+
// restore value
|
|
605
|
+
req.params[key] = paramCalled.value
|
|
606
|
+
|
|
607
|
+
// next param
|
|
608
|
+
return param(paramCalled.error)
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
called[key] = paramCalled = {
|
|
612
|
+
error: null,
|
|
613
|
+
match: paramVal,
|
|
614
|
+
value: paramVal
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
paramCallback()
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// single param callbacks
|
|
621
|
+
function paramCallback (err) {
|
|
622
|
+
const fn = paramCallbacks[paramIndex++]
|
|
623
|
+
|
|
624
|
+
// store updated value
|
|
625
|
+
paramCalled.value = req.params[key]
|
|
626
|
+
|
|
627
|
+
if (err) {
|
|
628
|
+
// store error
|
|
629
|
+
paramCalled.error = err
|
|
630
|
+
param(err)
|
|
631
|
+
return
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
if (!fn) return param()
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const ret = fn(req, res, paramCallback, paramVal, key)
|
|
638
|
+
if (ret instanceof Promise) {
|
|
639
|
+
ret.catch((error = new Error('Rejected promise')) => paramCallback(error))
|
|
640
|
+
}
|
|
641
|
+
} catch (e) {
|
|
642
|
+
paramCallback(e)
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
param()
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Restore obj props after function
|
|
651
|
+
*
|
|
652
|
+
* @private
|
|
653
|
+
*/
|
|
654
|
+
|
|
655
|
+
function restore (fn, obj, ...props) {
|
|
656
|
+
const vals = props.map(prop => obj[prop])
|
|
657
|
+
|
|
658
|
+
return function () {
|
|
659
|
+
// restore vals
|
|
660
|
+
for (let i = 0; i < props.length; i++) {
|
|
661
|
+
obj[props[i]] = vals[i]
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return fn.apply(this, arguments)
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Send an OPTIONS response.
|
|
670
|
+
*
|
|
671
|
+
* @private
|
|
672
|
+
*/
|
|
673
|
+
|
|
674
|
+
function sendOptionsResponse (res, methods) {
|
|
675
|
+
const options = Object.create(null)
|
|
676
|
+
|
|
677
|
+
// build unique method map
|
|
678
|
+
for (let i = 0; i < methods.length; i++) {
|
|
679
|
+
options[methods[i]] = true
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// construct the allow list
|
|
683
|
+
const allow = Object.keys(options).sort().join(', ')
|
|
684
|
+
|
|
685
|
+
// send response
|
|
686
|
+
res.setHeader('Allow', allow)
|
|
687
|
+
res.setHeader('Content-Length', Buffer.byteLength(allow))
|
|
688
|
+
res.setHeader('Content-Type', 'text/plain')
|
|
689
|
+
res.setHeader('X-Content-Type-Options', 'nosniff')
|
|
690
|
+
res.end(allow)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Try to send an OPTIONS response.
|
|
695
|
+
*
|
|
696
|
+
* @private
|
|
697
|
+
*/
|
|
698
|
+
|
|
699
|
+
function trySendOptionsResponse (res, methods, next) {
|
|
700
|
+
try {
|
|
701
|
+
sendOptionsResponse(res, methods)
|
|
702
|
+
} catch (err) {
|
|
703
|
+
next(err)
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
/**
|
|
708
|
+
* Wrap a function
|
|
709
|
+
*
|
|
710
|
+
* @private
|
|
711
|
+
*/
|
|
712
|
+
|
|
713
|
+
function wrap (old, fn) {
|
|
714
|
+
return function proxy (...args) {
|
|
715
|
+
fn.call(this, old, ...args)
|
|
716
|
+
}
|
|
717
|
+
}
|