fulmine.js 5.15.0 → 5.15.1
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/README.md +4 -0
- package/package.json +1 -1
- package/src/middlewares.js +69 -45
- package/src/parse-query.js +29 -1
- package/src/request.js +122 -57
- package/src/response.js +56 -13
- package/src/router.js +304 -78
- package/src/utils.js +15 -0
package/README.md
CHANGED
|
@@ -296,6 +296,10 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
296
296
|
|
|
297
297
|
## Differences from Express
|
|
298
298
|
|
|
299
|
+
What the two servers answer on the wire, probed from outside, malformed input and smuggling
|
|
300
|
+
attempts included: [fulmine.js on http-probe.com](https://www.http-probe.com/servers/fulmine-js.html)
|
|
301
|
+
against [express](https://www.http-probe.com/servers/express.html).
|
|
302
|
+
|
|
299
303
|
- `app.listen()` returns the app rather than a separate server object, and the app answers as an `http.Server`: `app instanceof http.Server` is true, which is what the graceful shutdown wrappers and the connection trackers look for. There is still no node server underneath, the socket belongs to µWS, so what is answered is the surface and not the plumbing. There: `close()`, `address()`, `listening`, `getConnections()`, `ref()`, `unref()`, `setTimeout()` and the `keepAliveTimeout` family. Not there: nothing emits `connection`, `request` or `upgrade`, `getConnections()` counts the requests in flight rather than sockets, and the timeouts belong to µWS and are set through `uwsOptions.idleTimeout`. Anything that wants to serve its own protocol on the socket, socket.io being the usual case, still wants `app.uwsApp`. Runnable: [`examples/graceful-shutdown.js`](./examples/graceful-shutdown.js).
|
|
300
304
|
- `x-powered-by` is disabled by default. Express sends `X-Powered-By: Express` unless you turn it off; Fulmine does not send it unless you turn it on with `app.set("x-powered-by", true)`. The header only tells anyone asking which framework is running.
|
|
301
305
|
- request body is only read for POST, PUT, PATCH and QUERY requests by default. You can add additional methods by setting `body methods` to array with uppercased methods.
|
package/package.json
CHANGED
package/src/middlewares.js
CHANGED
|
@@ -29,6 +29,20 @@ const qs = require("qs");
|
|
|
29
29
|
const parseQuery = require("./parse-query.js");
|
|
30
30
|
const { kGetSafe } = require("./usage.js");
|
|
31
31
|
const { AsyncResource } = require("async_hooks");
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* What AsyncResource.bind answers, without node's generic wrapper: that one builds a rest-args
|
|
35
|
+
* closure and defines properties onto it per call, ~1.9us on this node, where the resource plus
|
|
36
|
+
* an arrow through runInAsyncScope restores the same context for ~0.08. The type keeps the bound
|
|
37
|
+
* function's name, as node's does.
|
|
38
|
+
*
|
|
39
|
+
* @param {(...args: any[]) => any} fn called with at most one argument by every caller here
|
|
40
|
+
* @returns {(err?: any) => any}
|
|
41
|
+
*/
|
|
42
|
+
function bindContext(fn) {
|
|
43
|
+
const resource = new AsyncResource(fn.name || "bound-anonymous-fn");
|
|
44
|
+
return (err) => resource.runInAsyncScope(fn, undefined, err);
|
|
45
|
+
}
|
|
32
46
|
const {
|
|
33
47
|
fastQueryParse,
|
|
34
48
|
NullObject,
|
|
@@ -758,7 +772,7 @@ function serveStatic(root, options) {
|
|
|
758
772
|
return res.sendFile(
|
|
759
773
|
_path,
|
|
760
774
|
options,
|
|
761
|
-
|
|
775
|
+
bindContext((e) => {
|
|
762
776
|
if (e) {
|
|
763
777
|
next(options.fallthrough && FALLTHROUGH_STATUSES.has(e.status) ? undefined : e);
|
|
764
778
|
}
|
|
@@ -911,12 +925,14 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
911
925
|
}
|
|
912
926
|
|
|
913
927
|
const length = req._rawHeader("content-length");
|
|
928
|
+
// converted once: four sites read this number on the fast path
|
|
929
|
+
const lengthNumber = length === undefined ? NaN : +length;
|
|
914
930
|
|
|
915
931
|
// No content-length and no transfer-encoding means the request carries no body at all,
|
|
916
932
|
// and a body parser must leave it alone rather than parse nothing into an empty value.
|
|
917
933
|
// type-is applies this before matching the type, but the simpleType shortcut below
|
|
918
934
|
// compares strings directly and would otherwise skip the check.
|
|
919
|
-
if (req._rawHeader("transfer-encoding") === undefined && isNaN(
|
|
935
|
+
if (req._rawHeader("transfer-encoding") === undefined && Number.isNaN(lengthNumber)) {
|
|
920
936
|
return next();
|
|
921
937
|
}
|
|
922
938
|
|
|
@@ -960,7 +976,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
960
976
|
// {} for json and urlencoded, '' for text, an empty Buffer for raw - rather than leaving
|
|
961
977
|
// req.body as the placeholder object. there is nothing to read, so run the tail directly,
|
|
962
978
|
// and the verify hook still runs first: webhook signature checks rely on that
|
|
963
|
-
if (
|
|
979
|
+
if (lengthNumber === 0) {
|
|
964
980
|
req.bodyRead = true;
|
|
965
981
|
const empty = Buffer.alloc(0);
|
|
966
982
|
if (!runVerify(req, res, next, options, empty)) {
|
|
@@ -969,12 +985,12 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
969
985
|
return beforeReturn(req, res, next, options, empty, encoding);
|
|
970
986
|
}
|
|
971
987
|
|
|
972
|
-
// skip reading too large body
|
|
973
|
-
if (
|
|
988
|
+
// skip reading too large body; NaN compares false, so no declared length passes
|
|
989
|
+
if (lengthNumber > limit) {
|
|
974
990
|
return next(
|
|
975
991
|
bodyError("request entity too large", 413, "entity.too.large", {
|
|
976
|
-
expected:
|
|
977
|
-
length:
|
|
992
|
+
expected: lengthNumber,
|
|
993
|
+
length: lengthNumber,
|
|
978
994
|
limit: limit
|
|
979
995
|
})
|
|
980
996
|
);
|
|
@@ -1024,7 +1040,7 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1024
1040
|
// From here the body really gets read, and uWS delivers it on native callbacks that
|
|
1025
1041
|
// carry no async context, so this is the one continuation that has to be bound: an
|
|
1026
1042
|
// upstream middleware's AsyncLocalStorage must still be there when next runs
|
|
1027
|
-
next =
|
|
1043
|
+
next = bindContext(next);
|
|
1028
1044
|
|
|
1029
1045
|
// with nothing to decompress, uWS can collect the whole body in native code: one
|
|
1030
1046
|
// callback instead of one per chunk, the limit enforced before any byte reaches JS,
|
|
@@ -1032,8 +1048,8 @@ function createBodyParser(defaultType, beforeReturn, checkOptions, charsetPolicy
|
|
|
1032
1048
|
// returns, so a view over uWS's own memory is enough. A declared length was the
|
|
1033
1049
|
// original case; a chunked body accumulates in the same native vector and only loses
|
|
1034
1050
|
// the length check, since there is no declaration to hold it to
|
|
1035
|
-
const declared =
|
|
1036
|
-
const declaresLength = !isNaN(declared) && declared > 0;
|
|
1051
|
+
const declared = lengthNumber;
|
|
1052
|
+
const declaresLength = !Number.isNaN(declared) && declared > 0;
|
|
1037
1053
|
if (!req.receivedData && !inflate && req._res.collectBody && (declaresLength || isNaN(declared))) {
|
|
1038
1054
|
req.bodyRead = true;
|
|
1039
1055
|
req._res.collectBody(limit, (body) => {
|
|
@@ -1358,50 +1374,58 @@ const urlencoded = createBodyParser(
|
|
|
1358
1374
|
function (req, res, next, options, buf, encoding) {
|
|
1359
1375
|
try {
|
|
1360
1376
|
const body = decodeBody(buf, encoding);
|
|
1361
|
-
const count = parameterCount(body, options.parameterLimit);
|
|
1362
|
-
if (count === undefined) {
|
|
1363
|
-
return next(bodyError("too many parameters", 413, "parameters.too.many"));
|
|
1364
|
-
}
|
|
1365
1377
|
// Express 5 defaults extended to false, so nested keys need opting in
|
|
1366
1378
|
const extended = typeof options.extended !== "undefined" ? options.extended : false;
|
|
1367
1379
|
// qs has to know the charset itself for anything but utf-8, and the sentinel options
|
|
1368
1380
|
// change what a parse means, so those bodies skip the fast parsers
|
|
1369
1381
|
const needsQs = encoding !== "utf-8" || options.charsetSentinel || options.interpretNumericEntities;
|
|
1370
|
-
if (extended) {
|
|
1371
|
-
// the
|
|
1372
|
-
//
|
|
1373
|
-
//
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
allowPrototypes: true,
|
|
1393
|
-
arrayLimit: count + 1,
|
|
1394
|
-
depth: 0,
|
|
1395
|
-
strictDepth: true,
|
|
1382
|
+
if (!extended && !needsQs) {
|
|
1383
|
+
// the vendored parser, so an urlencoded body inspects like req.query does. The
|
|
1384
|
+
// parameter limit is enforced inside its scan, so the body is not walked twice;
|
|
1385
|
+
// assigned only when it held, so an overflow leaves req.body the placeholder
|
|
1386
|
+
const parsed = parseQuery(body, undefined, options.parameterLimit);
|
|
1387
|
+
if (parseQuery.overflow === true) {
|
|
1388
|
+
return next(bodyError("too many parameters", 413, "parameters.too.many"));
|
|
1389
|
+
}
|
|
1390
|
+
req.body = parsed;
|
|
1391
|
+
} else {
|
|
1392
|
+
const count = parameterCount(body, options.parameterLimit);
|
|
1393
|
+
if (count === undefined) {
|
|
1394
|
+
return next(bodyError("too many parameters", 413, "parameters.too.many"));
|
|
1395
|
+
}
|
|
1396
|
+
if (extended) {
|
|
1397
|
+
// the ceiling body-parser gives qs: the array limit rises to the parameter
|
|
1398
|
+
// count, so a form posting 150 array members still yields an array. count
|
|
1399
|
+
// counts "&" separators where body-parser counts parameters, hence the + 1
|
|
1400
|
+
const qsOptions = {
|
|
1401
|
+
...EXTENDED_QS_OPTIONS,
|
|
1402
|
+
depth: options.depth !== undefined ? options.depth : 32,
|
|
1403
|
+
arrayLimit: Math.max(100, count + 1),
|
|
1396
1404
|
charsetSentinel: options.charsetSentinel,
|
|
1397
1405
|
interpretNumericEntities: options.interpretNumericEntities,
|
|
1398
1406
|
charset: encoding,
|
|
1399
1407
|
parameterLimit: options.parameterLimit
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1408
|
+
};
|
|
1409
|
+
req.body = needsQs
|
|
1410
|
+
? Object.assign(Object.create(null), qs.parse(body, qsOptions))
|
|
1411
|
+
: fastQueryParse(body, qsOptions);
|
|
1412
|
+
} else {
|
|
1413
|
+
// body-parser's extended: false is still qs, with depth 0 and the count as
|
|
1414
|
+
// the array ceiling; only qs decodes latin1 percent escapes as latin1
|
|
1415
|
+
req.body = Object.assign(
|
|
1416
|
+
Object.create(null),
|
|
1417
|
+
qs.parse(body, {
|
|
1418
|
+
allowPrototypes: true,
|
|
1419
|
+
arrayLimit: count + 1,
|
|
1420
|
+
depth: 0,
|
|
1421
|
+
strictDepth: true,
|
|
1422
|
+
charsetSentinel: options.charsetSentinel,
|
|
1423
|
+
interpretNumericEntities: options.interpretNumericEntities,
|
|
1424
|
+
charset: encoding,
|
|
1425
|
+
parameterLimit: options.parameterLimit
|
|
1426
|
+
})
|
|
1427
|
+
);
|
|
1428
|
+
}
|
|
1405
1429
|
}
|
|
1406
1430
|
} catch (e) {
|
|
1407
1431
|
// qs reports a depth overflow as a RangeError with its own wording; body-parser
|
package/src/parse-query.js
CHANGED
|
@@ -34,17 +34,30 @@ const plusRegex = /\+/g;
|
|
|
34
34
|
* node's querystring.parse semantics on a null-prototype result: repeated keys accumulate into
|
|
35
35
|
* arrays, '+' is a space, percent sequences decode when present and stay literal when broken.
|
|
36
36
|
*
|
|
37
|
+
* `capture` collects the decoded pairs flat, key then value, so a caller can replay the stores
|
|
38
|
+
* without scanning again; a repeated key marks it invalid instead. See `get query`.
|
|
39
|
+
*
|
|
40
|
+
* `separatorLimit` refuses a body with that many "&" separators the way body-parser's
|
|
41
|
+
* parameterCount does, but inside this scan instead of a scan of its own: the overflow flag on
|
|
42
|
+
* the function is set, the partial result is to be discarded, and the caller answers 413.
|
|
43
|
+
*
|
|
37
44
|
* @param {string} input
|
|
45
|
+
* @param {string[] & {invalid?: boolean}} [capture]
|
|
46
|
+
* @param {number} [separatorLimit]
|
|
38
47
|
* @returns {Record<string, string | string[]>}
|
|
39
48
|
*/
|
|
40
|
-
function parseQuery(input) {
|
|
49
|
+
function parseQuery(input, capture, separatorLimit) {
|
|
41
50
|
const result = Object.create(null);
|
|
51
|
+
if (separatorLimit !== undefined) {
|
|
52
|
+
parseQuery.overflow = false;
|
|
53
|
+
}
|
|
42
54
|
|
|
43
55
|
if (typeof input !== "string") {
|
|
44
56
|
return result;
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
const inputLength = input.length;
|
|
60
|
+
let separators = 0;
|
|
48
61
|
let key;
|
|
49
62
|
let value = "";
|
|
50
63
|
let startingIndex = -1;
|
|
@@ -62,6 +75,12 @@ function parseQuery(input) {
|
|
|
62
75
|
|
|
63
76
|
// '&' or the end of the input closes the current pair
|
|
64
77
|
if (c === 38) {
|
|
78
|
+
// real separators only, not the synthetic closing one, counted exactly as
|
|
79
|
+
// parameterCount counts them
|
|
80
|
+
if (i !== inputLength && separatorLimit !== undefined && ++separators === separatorLimit) {
|
|
81
|
+
parseQuery.overflow = true;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
65
84
|
hasBothKeyValuePair = equalityIndex > startingIndex;
|
|
66
85
|
|
|
67
86
|
// the equality index doubles as the end of the key when there was no '='
|
|
@@ -92,7 +111,13 @@ function parseQuery(input) {
|
|
|
92
111
|
const currentValue = result[key];
|
|
93
112
|
if (currentValue === undefined) {
|
|
94
113
|
result[key] = value;
|
|
114
|
+
if (capture !== undefined) {
|
|
115
|
+
capture.push(key, value);
|
|
116
|
+
}
|
|
95
117
|
} else {
|
|
118
|
+
if (capture !== undefined) {
|
|
119
|
+
capture.invalid = true;
|
|
120
|
+
}
|
|
96
121
|
// value.pop is cheaper than Array.isArray here, as upstream measured
|
|
97
122
|
if (currentValue.pop) {
|
|
98
123
|
currentValue.push(value);
|
|
@@ -134,4 +159,7 @@ function parseQuery(input) {
|
|
|
134
159
|
return result;
|
|
135
160
|
}
|
|
136
161
|
|
|
162
|
+
// whether the last limited call hit its separator limit, see the parameter's doc
|
|
163
|
+
parseQuery.overflow = false;
|
|
164
|
+
|
|
137
165
|
module.exports = parseQuery;
|
package/src/request.js
CHANGED
|
@@ -229,6 +229,13 @@ function endsWithChunked(value) {
|
|
|
229
229
|
* @returns {boolean}
|
|
230
230
|
*/
|
|
231
231
|
function saysClose(value) {
|
|
232
|
+
// what clients actually send, almost always: two interned compares answer before the scan
|
|
233
|
+
if (value === "keep-alive") {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
if (value === "close") {
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
232
239
|
const length = value.length;
|
|
233
240
|
let at = 0;
|
|
234
241
|
while (at < length) {
|
|
@@ -662,6 +669,19 @@ module.exports = class Request extends LazyReadable {
|
|
|
662
669
|
*/
|
|
663
670
|
noEtag;
|
|
664
671
|
|
|
672
|
+
/**
|
|
673
|
+
* The decoded pairs of the first default-parser parse of req.query, flat key,value; false
|
|
674
|
+
* when it saw a repeated key, which a flat replay cannot reproduce. See `get query`.
|
|
675
|
+
* @type {string[]|false|undefined}
|
|
676
|
+
*/
|
|
677
|
+
_querySnap;
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* The raw string _querySnap came from: a url rewrite replaces _rawQuery.
|
|
681
|
+
* @type {string|undefined}
|
|
682
|
+
*/
|
|
683
|
+
_querySnapRaw;
|
|
684
|
+
|
|
665
685
|
/**
|
|
666
686
|
* Built for every request, which is why so little happens here. The headers are copied out
|
|
667
687
|
* because uWS only lends them for this call, everything derived from them waits until something
|
|
@@ -681,23 +701,24 @@ module.exports = class Request extends LazyReadable {
|
|
|
681
701
|
super();
|
|
682
702
|
this._res = res;
|
|
683
703
|
this._req = req;
|
|
684
|
-
// the plain field behind the `readable` accessor, written rather than set so a request that
|
|
685
|
-
// never streams never builds a stream
|
|
686
|
-
this._readableFlag = true;
|
|
687
704
|
if (skipHolder !== undefined && skipHolder.skipHeaders) {
|
|
688
705
|
// The chain behind this registration provably never reads a header, so instead of
|
|
689
|
-
// copying them all out of uWS the constructor asks for the
|
|
690
|
-
// framework itself: body framing, keep-alive, and
|
|
691
|
-
//
|
|
692
|
-
//
|
|
706
|
+
// copying them all out of uWS the constructor asks for the ones that steer the
|
|
707
|
+
// framework itself: body framing, keep-alive, and the conditional pair. A GET that
|
|
708
|
+
// does declare a body is the rare case, and the parsers and the stream want the
|
|
709
|
+
// whole picture, so it takes the full copy.
|
|
693
710
|
//
|
|
694
|
-
//
|
|
695
|
-
// seven
|
|
696
|
-
// napi crossing and the scan behind it is nothing,
|
|
697
|
-
// JS per header and grows, 1.16us at four
|
|
698
|
-
// do not cross, and the gap widens
|
|
699
|
-
// sends a dozen or more. The body
|
|
700
|
-
// which is 0.2us on a request that is
|
|
711
|
+
// A handful of named reads against one forEach looks like it should lose, and does
|
|
712
|
+
// not: measured at seven reads they were flat at 0.75us however many headers are on
|
|
713
|
+
// the wire, since each one is a napi crossing and the scan behind it is nothing,
|
|
714
|
+
// while the copy pays a hop back into JS per header and grows, 1.16us at four
|
|
715
|
+
// headers, 1.61 at eight, 2.90 at sixteen. They do not cross, and the gap widens
|
|
716
|
+
// exactly where real traffic lives, since a browser sends a dozen or more. The body
|
|
717
|
+
// case pays two reads and then copies anyway, which is 0.2us on a request that is
|
|
718
|
+
// about to read a body.
|
|
719
|
+
//
|
|
720
|
+
// accept is not read: nothing on a granted chain consumes it, the error and 404
|
|
721
|
+
// pages are fixed HTML that never negotiate.
|
|
701
722
|
const length = req.getHeader("content-length");
|
|
702
723
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
703
724
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
@@ -724,12 +745,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
724
745
|
this._connectionClose = true;
|
|
725
746
|
}
|
|
726
747
|
}
|
|
727
|
-
const accept = req.getHeader("accept");
|
|
728
|
-
if (accept !== "") {
|
|
729
|
-
entries.push("accept", accept);
|
|
730
|
-
}
|
|
731
748
|
// send consults freshness whatever the etag setting: if-none-match can be "*"
|
|
732
|
-
// and a handler may set a validator by hand, so the conditional
|
|
749
|
+
// and a handler may set a validator by hand, so the conditional pair has to be
|
|
733
750
|
// really absent rather than merely uncopied
|
|
734
751
|
const ifNoneMatch = req.getHeader("if-none-match");
|
|
735
752
|
if (ifNoneMatch !== "") {
|
|
@@ -739,9 +756,13 @@ module.exports = class Request extends LazyReadable {
|
|
|
739
756
|
if (ifModifiedSince !== "") {
|
|
740
757
|
entries.push("if-modified-since", ifModifiedSince);
|
|
741
758
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
759
|
+
// fresh() answers false before it reads cache-control unless a conditional
|
|
760
|
+
// arrived, so the read only pays on the requests that can use it
|
|
761
|
+
if (ifNoneMatch !== "" || ifModifiedSince !== "") {
|
|
762
|
+
const cacheControl = req.getHeader("cache-control");
|
|
763
|
+
if (cacheControl !== "") {
|
|
764
|
+
entries.push("cache-control", cacheControl);
|
|
765
|
+
}
|
|
745
766
|
}
|
|
746
767
|
}
|
|
747
768
|
} else {
|
|
@@ -802,18 +823,26 @@ module.exports = class Request extends LazyReadable {
|
|
|
802
823
|
this._opPath = this._path;
|
|
803
824
|
this._originalPath = this._path;
|
|
804
825
|
const rawMethod = req.getCaseSensitiveMethod();
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
826
|
+
if (skipHolder !== undefined && rawMethod === skipHolder.method) {
|
|
827
|
+
// the registration's constant, byte for byte; any other spelling, "get" that µWS
|
|
828
|
+
// folded here included, takes the full check below
|
|
829
|
+
this.method = rawMethod;
|
|
830
|
+
this._isOptions = skipHolder.isOptions;
|
|
831
|
+
this._isHead = skipHolder.isHead;
|
|
832
|
+
} else {
|
|
833
|
+
this.method = rawMethod.toUpperCase();
|
|
834
|
+
// node's parser knows a fixed set and refuses everything else; µWS takes the token
|
|
835
|
+
// as it finds it, so a request line is anything with a space in it. Compared before
|
|
836
|
+
// the uppercasing on purpose: a method is case sensitive, node refuses "post", and
|
|
837
|
+
// µWS folds it to POST and serves it. Only asked of a method the framework cannot
|
|
838
|
+
// route anyway, since a route can only be registered for one of these, see the loop
|
|
839
|
+
// that builds the verb methods at the end of router.js
|
|
840
|
+
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
841
|
+
this._mustRefuse = true;
|
|
842
|
+
}
|
|
843
|
+
this._isOptions = this.method === "OPTIONS";
|
|
844
|
+
this._isHead = this.method === "HEAD";
|
|
814
845
|
}
|
|
815
|
-
this._isOptions = this.method === "OPTIONS";
|
|
816
|
-
this._isHead = this.method === "HEAD";
|
|
817
846
|
}
|
|
818
847
|
// what the router last saw as the method. A middleware assigning another one is a rewrite,
|
|
819
848
|
// which express honours because it reads req.method at every layer, and dispatch compares
|
|
@@ -839,9 +868,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
839
868
|
// null for the same reason as the two above: a request that never enters a mount never
|
|
840
869
|
// needs either array, and the push sites materialize them
|
|
841
870
|
this._stack = null;
|
|
842
|
-
// how many characters of _originalPath the mounts entered so far have taken, which is
|
|
843
|
-
// where baseUrl ends and the path below them begins
|
|
844
|
-
this._consumed = 0;
|
|
845
871
|
// whether one of them took a trailing slash, which only a RegExp mount can: see baseUrl
|
|
846
872
|
this._mountSlash = false;
|
|
847
873
|
this._paramStack = null;
|
|
@@ -1039,7 +1065,11 @@ module.exports = class Request extends LazyReadable {
|
|
|
1039
1065
|
// slash, a registered path having had it removed, so almost every request answers above.
|
|
1040
1066
|
let out = "";
|
|
1041
1067
|
let at = 0;
|
|
1042
|
-
for (
|
|
1068
|
+
for (let taken of this._stack) {
|
|
1069
|
+
// negative marks a mount that consumed the whole path, see the push in runRoute
|
|
1070
|
+
if (taken < 0) {
|
|
1071
|
+
taken = -taken;
|
|
1072
|
+
}
|
|
1043
1073
|
const piece = this._originalPath.slice(at, at + taken);
|
|
1044
1074
|
at += taken;
|
|
1045
1075
|
out += piece.charCodeAt(taken - 1) === 0x2f ? piece.slice(0, -1) : piece;
|
|
@@ -1199,18 +1229,30 @@ module.exports = class Request extends LazyReadable {
|
|
|
1199
1229
|
* path, and req.query reflects the new query string. The assigned url is relative to the
|
|
1200
1230
|
* mount the request is currently in, as it is in express, so the absolute path is rebuilt
|
|
1201
1231
|
* from the piece the mounts had consumed.
|
|
1232
|
+
*
|
|
1233
|
+
* @param {boolean} [leavingMount] the caller is popping the mount the rewrite happened in
|
|
1202
1234
|
*/
|
|
1203
|
-
_absorbUrlRewrite() {
|
|
1204
|
-
const
|
|
1205
|
-
|
|
1206
|
-
const newPath = queryIndex === -1 ? newUrl : newUrl.slice(0, queryIndex);
|
|
1235
|
+
_absorbUrlRewrite(leavingMount) {
|
|
1236
|
+
const assignedUrl = String(this.url);
|
|
1237
|
+
let newUrl = assignedUrl;
|
|
1207
1238
|
// the prefix the mounts consumed: everything of the absolute path the relative one was not
|
|
1208
1239
|
const lastQueryIndex = this._lastUrl.indexOf("?");
|
|
1209
1240
|
const oldPath = lastQueryIndex === -1 ? this._lastUrl : this._lastUrl.slice(0, lastQueryIndex);
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1241
|
+
let prefix;
|
|
1242
|
+
if (oldPath === "/" && !this._originalPath.endsWith("/")) {
|
|
1243
|
+
prefix = this._originalPath;
|
|
1244
|
+
// express's slashAdded restore, applied where express applies it, on the way out of a
|
|
1245
|
+
// mount that consumed the whole path: the "/" the middleware saw was invented, and the
|
|
1246
|
+
// rejoin strips the first character of whatever was assigned ("/found" + "target" is
|
|
1247
|
+
// "/foundtarget"). Inside the mount the assigned url routes as it is, as express does
|
|
1248
|
+
if (leavingMount === true) {
|
|
1249
|
+
newUrl = newUrl.slice(1);
|
|
1250
|
+
}
|
|
1251
|
+
} else {
|
|
1252
|
+
prefix = this._originalPath.slice(0, this._originalPath.length - oldPath.length);
|
|
1253
|
+
}
|
|
1254
|
+
const queryIndex = newUrl.indexOf("?");
|
|
1255
|
+
const newPath = queryIndex === -1 ? newUrl : newUrl.slice(0, queryIndex);
|
|
1214
1256
|
this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
|
|
1215
1257
|
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
1216
1258
|
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
@@ -1220,7 +1262,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1220
1262
|
this._opPath = newPath;
|
|
1221
1263
|
this._opPathLower = null;
|
|
1222
1264
|
this._mayFailDecode = null;
|
|
1223
|
-
|
|
1265
|
+
// the assigned string, not the mangled one: the compare against req.url must go quiet or
|
|
1266
|
+
// the next hop absorbs the same rewrite again with a different prefix
|
|
1267
|
+
this._lastUrl = assignedUrl;
|
|
1224
1268
|
}
|
|
1225
1269
|
|
|
1226
1270
|
/**
|
|
@@ -1251,15 +1295,16 @@ module.exports = class Request extends LazyReadable {
|
|
|
1251
1295
|
* the parse cached and handed out as itself, the sanitised value leaked into req.query here and
|
|
1252
1296
|
* a handler written against express read a trimmed value where express gives it the raw one.
|
|
1253
1297
|
*
|
|
1254
|
-
* And that is why there is no cache: the fresh object comes from
|
|
1255
|
-
* not from copying a kept parse. As first shipped this was parse-once-copy-per-read,
|
|
1256
|
-
* copy was the expensive half: Object.assign between null-prototype objects, which
|
|
1257
|
-
* V8's dictionary mode, measured 638ns for a two-parameter query where parsing the
|
|
1258
|
-
* measures 119ns, and on a benchmark whose every request carries such a query it
|
|
1259
|
-
* of CPU per request, which a public arena saw as -8% on its query-carrying rows.
|
|
1260
|
-
*
|
|
1261
|
-
*
|
|
1262
|
-
*
|
|
1298
|
+
* And that is why there is no cache of the object: the fresh object comes from the raw
|
|
1299
|
+
* string, not from copying a kept parse. As first shipped this was parse-once-copy-per-read,
|
|
1300
|
+
* and the copy was the expensive half: Object.assign between null-prototype objects, which
|
|
1301
|
+
* live in V8's dictionary mode, measured 638ns for a two-parameter query where parsing the
|
|
1302
|
+
* same string measures 119ns, and on a benchmark whose every request carries such a query it
|
|
1303
|
+
* cost +1.5us of CPU per request, which a public arena saw as -8% on its query-carrying rows.
|
|
1304
|
+
*
|
|
1305
|
+
* The default parser does keep the decoded pairs of its first parse, and a later read of the
|
|
1306
|
+
* same raw string replays the stores into a fresh null-prototype object: identical output,
|
|
1307
|
+
* still nothing shared between reads. A repeated key cannot be replayed and re-parses.
|
|
1263
1308
|
*
|
|
1264
1309
|
* @returns {Record<string, any>}
|
|
1265
1310
|
*/
|
|
@@ -1277,7 +1322,25 @@ module.exports = class Request extends LazyReadable {
|
|
|
1277
1322
|
return Object.create(null);
|
|
1278
1323
|
}
|
|
1279
1324
|
if (qp === parseQuery) {
|
|
1280
|
-
|
|
1325
|
+
const raw = this._rawQuery;
|
|
1326
|
+
if (this._querySnapRaw === raw) {
|
|
1327
|
+
const snap = this._querySnap;
|
|
1328
|
+
if (snap === false) {
|
|
1329
|
+
return parseQuery(raw);
|
|
1330
|
+
}
|
|
1331
|
+
const out = Object.create(null);
|
|
1332
|
+
const pairs = /** @type {string[]} */ (snap);
|
|
1333
|
+
for (let i = 0, len = pairs.length; i < len; i += 2) {
|
|
1334
|
+
out[pairs[i]] = pairs[i + 1];
|
|
1335
|
+
}
|
|
1336
|
+
return out;
|
|
1337
|
+
}
|
|
1338
|
+
/** @type {string[] & {invalid?: boolean}} */
|
|
1339
|
+
const capture = [];
|
|
1340
|
+
const out = parseQuery(raw, capture);
|
|
1341
|
+
this._querySnapRaw = raw;
|
|
1342
|
+
this._querySnap = capture.invalid === true ? false : capture;
|
|
1343
|
+
return out;
|
|
1281
1344
|
}
|
|
1282
1345
|
if (qp === fastQueryParse) {
|
|
1283
1346
|
return Object.assign(Object.create(null), fastQueryParse(this._rawQuery));
|
|
@@ -1686,7 +1749,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1686
1749
|
const value = entries[index + 1];
|
|
1687
1750
|
// lowercase by the entries' contract, see the field declaration
|
|
1688
1751
|
const key = entries[index];
|
|
1689
|
-
|
|
1752
|
+
// own values are never undefined, so the read answers "absent" without the hasOwn
|
|
1753
|
+
// call; a prototype-named header reads truthy and still takes the hasOwn check
|
|
1754
|
+
if (headers[key] !== undefined && Object.hasOwn(headers, key)) {
|
|
1690
1755
|
if (discardedDuplicates.has(key)) {
|
|
1691
1756
|
continue;
|
|
1692
1757
|
}
|
package/src/response.js
CHANGED
|
@@ -72,15 +72,37 @@ const symbols = Object.getOwnPropertySymbols(outgoingMessage);
|
|
|
72
72
|
// if a future node renames it, fall back to a private symbol rather than writing a property
|
|
73
73
|
// literally named "undefined", which is what indexing with undefined would do
|
|
74
74
|
const kOutHeaders = symbols.find((s) => s.toString() === "Symbol(kOutHeaders)") ?? Symbol("kOutHeaders");
|
|
75
|
+
// node's emitters tombstone a removed listener's slot instead of deleting it when this flag is
|
|
76
|
+
// set, which is what keeps _events in a stable shape. EventEmitter.init sets it, and never runs
|
|
77
|
+
// for the lazily-materialized response, so it is set by hand; a future rename degrades to the
|
|
78
|
+
// delete, not to an error
|
|
79
|
+
const kShapeMode =
|
|
80
|
+
Object.getOwnPropertySymbols(new EventEmitter()).find((s) => s.toString() === "Symbol(shapeMode)") ??
|
|
81
|
+
Symbol("shapeMode");
|
|
82
|
+
// names setHeader has validated and lowercased, so the constant names middleware writes per
|
|
83
|
+
// request are one Map hit. Insert-only after validation, bounded; only setHeader may insert,
|
|
84
|
+
// the never-throwing readers keep their plain toLowerCase
|
|
85
|
+
const VALIDATED_HEADER_NAMES = new Map();
|
|
75
86
|
const HIGH_WATERMARK = 128 * 1024;
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
const STATUSES_WITHOUT_BODY = new Set([204, 304]);
|
|
87
|
+
// the exact string json() writes, so send() can skip recomputing the charset on it
|
|
88
|
+
const JSON_UTF8 = "application/json; charset=utf-8";
|
|
79
89
|
// send's ceiling for maxAge, one year in milliseconds. Anything larger is clamped to it rather
|
|
80
90
|
// than written out, since a year is already longer than any cache will honour.
|
|
81
91
|
const MAX_MAXAGE = 60 * 60 * 24 * 365 * 1000;
|
|
82
92
|
|
|
83
93
|
class Socket extends EventEmitter {
|
|
94
|
+
/**
|
|
95
|
+
* The Socket's own error listener, shared across sockets: an error on the stand-in closes it,
|
|
96
|
+
* which is the close the connection trackers wait for. EventEmitter calls it with this = the
|
|
97
|
+
* emitter.
|
|
98
|
+
*
|
|
99
|
+
* @this {any}
|
|
100
|
+
* @param {any} err
|
|
101
|
+
*/
|
|
102
|
+
static _onError(err) {
|
|
103
|
+
this.emit("close");
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/**
|
|
85
107
|
* Enough of a node socket for the middleware that reaches for one. There is no socket object
|
|
86
108
|
* in uWS to hand over, so this stands in and forwards what it can to the response.
|
|
@@ -90,10 +112,10 @@ class Socket extends EventEmitter {
|
|
|
90
112
|
constructor(response) {
|
|
91
113
|
super();
|
|
92
114
|
this.response = response;
|
|
115
|
+
this[kShapeMode] = true;
|
|
93
116
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
});
|
|
117
|
+
// shared, not an arrow: one per process instead of one per materialized socket
|
|
118
|
+
this.on("error", Socket._onError);
|
|
97
119
|
}
|
|
98
120
|
|
|
99
121
|
/** Whether anything more can be written, which stops being true once the response is done. */
|
|
@@ -276,6 +298,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
276
298
|
drain: undefined
|
|
277
299
|
};
|
|
278
300
|
this._eventsCount = 0;
|
|
301
|
+
// tombstone removed listeners instead of deleting the key, as node's own streams do: the
|
|
302
|
+
// delete flipped this literal to dictionary mode on every on-finished cancel
|
|
303
|
+
this[kShapeMode] = true;
|
|
304
|
+
// on-finished stores its state here; declared so that store is not a shape change
|
|
305
|
+
this.__onFinished = null;
|
|
279
306
|
this._req = req;
|
|
280
307
|
// linked here rather than by the caller: the pair is built together, and a field the
|
|
281
308
|
// constructor leaves unset is a shape change on whoever assigns it first
|
|
@@ -801,7 +828,9 @@ module.exports = class Response extends LazyWritable {
|
|
|
801
828
|
// without one writes "Content-Length: 9223372036854775808" onto a 204. Those two paths keep
|
|
802
829
|
// µWS's own rule, which closes for a bare "close" and not for a list.
|
|
803
830
|
const closeConnection = this.req._connectionClose === true;
|
|
804
|
-
|
|
831
|
+
// 204 and 304 carry no body, so no Content-Length may describe one either; 1xx is the
|
|
832
|
+
// third case, by range
|
|
833
|
+
if (this.statusCode === 204 || this.statusCode === 304 || this.statusCode < 200) {
|
|
805
834
|
// no body and no length describing one, whatever the caller passed. node decides
|
|
806
835
|
// this the same way, from the status alone, so res.status(304).end("x") sends the
|
|
807
836
|
// status and nothing else on either.
|
|
@@ -905,9 +934,10 @@ module.exports = class Response extends LazyWritable {
|
|
|
905
934
|
if (!skipContentType) {
|
|
906
935
|
this.headers["content-type"] = "text/html; charset=utf-8";
|
|
907
936
|
}
|
|
908
|
-
} else if (typeof contentType === "string") {
|
|
937
|
+
} else if (typeof contentType === "string" && contentType !== JSON_UTF8) {
|
|
909
938
|
// replaced, not only added: the body goes out as utf-8, so a content-type saying
|
|
910
|
-
// iso-8859-1 would be describing bytes that are not there.
|
|
939
|
+
// iso-8859-1 would be describing bytes that are not there. The json() literal is
|
|
940
|
+
// already exactly that, so the common res.json answer skips the recomputation
|
|
911
941
|
this.headers["content-type"] = withUtf8Charset(contentType);
|
|
912
942
|
}
|
|
913
943
|
} else {
|
|
@@ -1374,7 +1404,16 @@ module.exports = class Response extends LazyWritable {
|
|
|
1374
1404
|
if (this.headersSent) {
|
|
1375
1405
|
throw new Error("Cannot set headers after they are sent to the client");
|
|
1376
1406
|
}
|
|
1377
|
-
|
|
1407
|
+
// one Map hit for a name already validated and lowercased: middleware writes the same
|
|
1408
|
+
// constant names on every request. Insert-only after validation, so no bad name can enter
|
|
1409
|
+
let key = VALIDATED_HEADER_NAMES.get(field);
|
|
1410
|
+
if (key === undefined) {
|
|
1411
|
+
validateHeaderName(field);
|
|
1412
|
+
key = field.toLowerCase();
|
|
1413
|
+
if (VALIDATED_HEADER_NAMES.size < 512) {
|
|
1414
|
+
VALIDATED_HEADER_NAMES.set(field, key);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1378
1417
|
if (value === undefined) {
|
|
1379
1418
|
/** @type {NodeJS.ErrnoException} */
|
|
1380
1419
|
const err = new TypeError(`Invalid value "undefined" for header "${field}"`);
|
|
@@ -1388,7 +1427,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1388
1427
|
// catch it
|
|
1389
1428
|
const out = Array.isArray(value) ? value.map(String) : String(value);
|
|
1390
1429
|
validateHeaderValue(field, out);
|
|
1391
|
-
this.headers[
|
|
1430
|
+
this.headers[key] = out;
|
|
1392
1431
|
return this;
|
|
1393
1432
|
}
|
|
1394
1433
|
|
|
@@ -1737,7 +1776,11 @@ module.exports = class Response extends LazyWritable {
|
|
|
1737
1776
|
* @param {string} field
|
|
1738
1777
|
*/
|
|
1739
1778
|
removeHeader(field) {
|
|
1740
|
-
|
|
1779
|
+
const key = field.toLowerCase();
|
|
1780
|
+
// the delete is a runtime call, and helmet removes a header most responses never carry
|
|
1781
|
+
if (key in this.headers) {
|
|
1782
|
+
delete this.headers[key];
|
|
1783
|
+
}
|
|
1741
1784
|
}
|
|
1742
1785
|
|
|
1743
1786
|
/**
|
|
@@ -1912,7 +1955,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1912
1955
|
*/
|
|
1913
1956
|
json(body) {
|
|
1914
1957
|
if (!this.headers["content-type"]) {
|
|
1915
|
-
this.headers["content-type"] =
|
|
1958
|
+
this.headers["content-type"] = JSON_UTF8;
|
|
1916
1959
|
}
|
|
1917
1960
|
const hot = this.app._hot();
|
|
1918
1961
|
return this.send(stringify(body, hot.jsonReplacer, hot.jsonSpaces, hot.jsonEscape));
|
package/src/router.js
CHANGED
|
@@ -242,36 +242,45 @@ class Walk {
|
|
|
242
242
|
// frozen here, once per scan: _pathMatches reads the two flags as bare fields, and
|
|
243
243
|
// calling this per route measured 0.45us of a scan of four hundred
|
|
244
244
|
router._freezeRoutingFlags();
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
r
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
//
|
|
262
|
-
//
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
245
|
+
if (routes === router._routes) {
|
|
246
|
+
// the router's own table has an index over its literal routes, so the scan visits
|
|
247
|
+
// the handful that could match instead of every one, see _scanFrom
|
|
248
|
+
routeIndex = router._scanFrom(req, routeIndex, mayFailDecode);
|
|
249
|
+
} else {
|
|
250
|
+
// a compiled chain's own array, always short: the linear scan stays.
|
|
251
|
+
// Written out rather than through a predicate handed to findIndexStartingFrom,
|
|
252
|
+
// which was one closure per hop of every request not on a compiled chain
|
|
253
|
+
const method = req.method;
|
|
254
|
+
const length = routes.length;
|
|
255
|
+
for (; routeIndex < length; routeIndex++) {
|
|
256
|
+
const r = routes[routeIndex];
|
|
257
|
+
// A HEAD request enters a route whose path matched even when its verb cannot
|
|
258
|
+
// serve one: express exempts HEAD from the method check ("if (!hasMethod &&
|
|
259
|
+
// method !== 'HEAD')" in router/index.js), so the layer's parameters are
|
|
260
|
+
// captured and its param() callbacks run before the route is dropped. Only
|
|
261
|
+
// asked when the router has callbacks to run, since entering a route to step
|
|
262
|
+
// straight back out of it is otherwise pure cost. runRoute steps over it.
|
|
263
|
+
if (!(
|
|
264
|
+
r.all ||
|
|
265
|
+
r.method === method ||
|
|
266
|
+
req._isOptions ||
|
|
267
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
268
|
+
)) {
|
|
269
|
+
// taken only to fail: _preprocessRequest decodes again and turns it into
|
|
270
|
+
// the error, so the handlers of a route this request cannot run never see it
|
|
271
|
+
if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
272
274
|
continue;
|
|
273
275
|
}
|
|
274
|
-
|
|
276
|
+
if (router._pathMatches(r, req)) {
|
|
277
|
+
// matched, and then stepped over: a body parser this request gets nothing
|
|
278
|
+
// out of costs a hop and answers with next() at the end of it
|
|
279
|
+
if (r.bodyParserOnly === true && stepsOver(r, req)) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
275
284
|
}
|
|
276
285
|
}
|
|
277
286
|
}
|
|
@@ -402,7 +411,11 @@ class Walk {
|
|
|
402
411
|
useApp(req, route.mountApp);
|
|
403
412
|
}
|
|
404
413
|
const taken = mountPrefixLength(route, req);
|
|
405
|
-
|
|
414
|
+
// pushed negative when this mount consumes the whole remaining path: express invents
|
|
415
|
+
// the "/" the routes below see, and the pop has to know, see leaveHop and issue #17
|
|
416
|
+
(req._stack ??= []).push(
|
|
417
|
+
taken !== 0 && req._consumed + taken === req._originalPath.length ? -taken : taken
|
|
418
|
+
);
|
|
406
419
|
// a use with no path consumes nothing, so everything below would work out the values
|
|
407
420
|
// that are already there. Only skipped without a trailing slash, where the rules about
|
|
408
421
|
// one cannot bite. An application is mostly pathless middleware, and this is per hop
|
|
@@ -466,6 +479,68 @@ class Walk {
|
|
|
466
479
|
return this.step(undefined);
|
|
467
480
|
}
|
|
468
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Leaves the route the walk is on: the mount pop, the router hand-back, and the hop to the
|
|
484
|
+
* route after. Out of step so the commonest hop, callbacks exhausted by a plain next(), goes
|
|
485
|
+
* here without re-running step's prologue and compares.
|
|
486
|
+
*
|
|
487
|
+
* @param {boolean} isRouter next("router") rather than next("route")
|
|
488
|
+
*/
|
|
489
|
+
leaveHop(isRouter) {
|
|
490
|
+
const req = this.req;
|
|
491
|
+
const route = this.route;
|
|
492
|
+
if (route.use && !route.keepMount) {
|
|
493
|
+
const pushed = req._stack.pop();
|
|
494
|
+
const taken = pushed < 0 ? -pushed : pushed;
|
|
495
|
+
// a rewrite done inside this middleware is taken now: the pop below recomputes
|
|
496
|
+
// req.url from the original path and would silently revert it. The slashAdded
|
|
497
|
+
// mangle belongs to the mount that consumed a prefix, not to a pathless use
|
|
498
|
+
if (req.url !== req._lastUrl) {
|
|
499
|
+
req._absorbUrlRewrite(taken !== 0);
|
|
500
|
+
req._consumed -= taken;
|
|
501
|
+
setMountedPath(req);
|
|
502
|
+
} else {
|
|
503
|
+
if (pushed < 0 && req._originalPath.length > req._consumed) {
|
|
504
|
+
// a rewrite below this mount left a remainder where entry had none: express
|
|
505
|
+
// strips the first character of it when it rejoins, see issue #17
|
|
506
|
+
req._originalPath =
|
|
507
|
+
req._originalPath.slice(0, req._consumed) + req._originalPath.slice(req._consumed + 1);
|
|
508
|
+
req._mayFailDecode = null;
|
|
509
|
+
}
|
|
510
|
+
if (taken !== 0) {
|
|
511
|
+
// a pathless use consumed nothing and rewrote nothing, so the recompute would
|
|
512
|
+
// write back the very values it reads
|
|
513
|
+
req._consumed -= taken;
|
|
514
|
+
setMountedPath(req);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
restoreApp(route, req);
|
|
518
|
+
}
|
|
519
|
+
if (isRouter) {
|
|
520
|
+
if (this.skipCheck) {
|
|
521
|
+
// on a compiled chain, leaving the router is what running out of chain
|
|
522
|
+
// already means: ordinary routing takes over after the mount. With no
|
|
523
|
+
// mount in the chain the router being left is the app's own, and nothing
|
|
524
|
+
// of it may run afterwards, not even a middleware registered later
|
|
525
|
+
if (this.skipUntil?.keepMount) {
|
|
526
|
+
return this.dispatch(this.routes.length);
|
|
527
|
+
}
|
|
528
|
+
return this.resolve(false);
|
|
529
|
+
}
|
|
530
|
+
// out of this router entirely, so whoever mounted it carries on after the
|
|
531
|
+
// mount. The app's own walk has nobody after it, and answers 404
|
|
532
|
+
return this.resolve(false);
|
|
533
|
+
}
|
|
534
|
+
req.routeCount++;
|
|
535
|
+
// dispatch is a plain call, so a synchronous throw would escape here instead of
|
|
536
|
+
// rejecting, as it used to when this recursed through the async _routeRequest
|
|
537
|
+
try {
|
|
538
|
+
return this.dispatch(this.routeIndex + 1);
|
|
539
|
+
} catch (err) {
|
|
540
|
+
return this.reject(err);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
469
544
|
/**
|
|
470
545
|
* One hop, which is what next() does: with nothing, run the route's next callback; with "route",
|
|
471
546
|
* leave the route; with anything else, remember it as the error and carry on.
|
|
@@ -479,39 +554,7 @@ class Walk {
|
|
|
479
554
|
const router = this.router;
|
|
480
555
|
if (thingamabob) {
|
|
481
556
|
if (thingamabob === "route" || thingamabob === "router") {
|
|
482
|
-
|
|
483
|
-
// a rewrite done inside this middleware is taken now: the pop below recomputes
|
|
484
|
-
// req.url from the original path and would silently revert it
|
|
485
|
-
if (req.url !== req._lastUrl) {
|
|
486
|
-
req._absorbUrlRewrite();
|
|
487
|
-
}
|
|
488
|
-
req._consumed -= req._stack.pop();
|
|
489
|
-
setMountedPath(req);
|
|
490
|
-
restoreApp(route, req);
|
|
491
|
-
}
|
|
492
|
-
if (thingamabob === "router") {
|
|
493
|
-
if (this.skipCheck) {
|
|
494
|
-
// on a compiled chain, leaving the router is what running out of chain
|
|
495
|
-
// already means: ordinary routing takes over after the mount. With no
|
|
496
|
-
// mount in the chain the router being left is the app's own, and nothing
|
|
497
|
-
// of it may run afterwards, not even a middleware registered later
|
|
498
|
-
if (this.skipUntil?.keepMount) {
|
|
499
|
-
return this.dispatch(this.routes.length);
|
|
500
|
-
}
|
|
501
|
-
return this.resolve(false);
|
|
502
|
-
}
|
|
503
|
-
// out of this router entirely, so whoever mounted it carries on after the
|
|
504
|
-
// mount. The app's own walk has nobody after it, and answers 404
|
|
505
|
-
return this.resolve(false);
|
|
506
|
-
}
|
|
507
|
-
req.routeCount++;
|
|
508
|
-
// dispatch is a plain call, so a synchronous throw would escape here instead of
|
|
509
|
-
// rejecting, as it used to when this recursed through the async _routeRequest
|
|
510
|
-
try {
|
|
511
|
-
return this.dispatch(this.routeIndex + 1);
|
|
512
|
-
} catch (err) {
|
|
513
|
-
return this.reject(err);
|
|
514
|
-
}
|
|
557
|
+
return this.leaveHop(thingamabob === "router");
|
|
515
558
|
} else {
|
|
516
559
|
req._error = thingamabob;
|
|
517
560
|
req._errorKey = route.routeKey;
|
|
@@ -521,7 +564,7 @@ class Walk {
|
|
|
521
564
|
const kind = route.callbackKinds[this.callbackIndex];
|
|
522
565
|
const callback = route.callbacks[this.callbackIndex++];
|
|
523
566
|
if (!callback) {
|
|
524
|
-
return this.
|
|
567
|
+
return this.leaveHop(false);
|
|
525
568
|
}
|
|
526
569
|
// skipping routes we already went through via optimized path. Before the Router branch
|
|
527
570
|
// below and not after it: a mount whose chain was compiled has already run, and running it
|
|
@@ -715,6 +758,10 @@ function mountPrefixLength(route, req) {
|
|
|
715
758
|
if (route.pattern === EMPTY_REGEX) {
|
|
716
759
|
return 0;
|
|
717
760
|
}
|
|
761
|
+
// the registration-time constant of a literal mount, exec-free. See createRoute
|
|
762
|
+
if (route.mountLen !== undefined) {
|
|
763
|
+
return route.mountLen;
|
|
764
|
+
}
|
|
718
765
|
if (typeof route.pattern === "string") {
|
|
719
766
|
return route.pattern.length;
|
|
720
767
|
}
|
|
@@ -801,6 +848,61 @@ function mergesParams(route, fallback) {
|
|
|
801
848
|
return Boolean(owner?._settings?.mergeParams);
|
|
802
849
|
}
|
|
803
850
|
|
|
851
|
+
// shared empty candidate list, so _scanFrom never tests for a missing map entry twice
|
|
852
|
+
const EMPTY_INDICES = /** @type {number[]} */ ([]);
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* The generic scan's index over a router's literal routes: route positions by folded pattern, so
|
|
856
|
+
* a scan visits the routes registered for this exact path instead of comparing every one. String
|
|
857
|
+
* patterns are pure literals, everything else, "/*" included, stays in alwaysVisit and is still
|
|
858
|
+
* matched per request by _pathMatches.
|
|
859
|
+
*
|
|
860
|
+
* @param {any[]} routes the router's own table
|
|
861
|
+
* @param {boolean} caseFlag the frozen case-sensitivity flag
|
|
862
|
+
* @returns {{map: Map<string, number[]>, alwaysVisit: number[]}}
|
|
863
|
+
*/
|
|
864
|
+
function buildLiteralIndex(routes, caseFlag) {
|
|
865
|
+
const map = new Map();
|
|
866
|
+
const alwaysVisit = [];
|
|
867
|
+
for (let i = 0; i < routes.length; i++) {
|
|
868
|
+
const pattern = routes[i].pattern;
|
|
869
|
+
if (typeof pattern === "string" && pattern !== "/*") {
|
|
870
|
+
const key = caseFlag ? pattern : routes[i].patternLower;
|
|
871
|
+
const list = map.get(key);
|
|
872
|
+
if (list === undefined) {
|
|
873
|
+
map.set(key, [i]);
|
|
874
|
+
} else {
|
|
875
|
+
list.push(i);
|
|
876
|
+
}
|
|
877
|
+
} else {
|
|
878
|
+
alwaysVisit.push(i);
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
return { map, alwaysVisit };
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
/**
|
|
885
|
+
* The position of the first value >= from in an ascending list, which is list.length when there
|
|
886
|
+
* is none: where a scan resuming at `from` enters a candidate list.
|
|
887
|
+
*
|
|
888
|
+
* @param {number[]} list
|
|
889
|
+
* @param {number} from
|
|
890
|
+
* @returns {number}
|
|
891
|
+
*/
|
|
892
|
+
function firstAtLeast(list, from) {
|
|
893
|
+
let low = 0;
|
|
894
|
+
let high = list.length;
|
|
895
|
+
while (low < high) {
|
|
896
|
+
const mid = (low + high) >> 1;
|
|
897
|
+
if (list[mid] < from) {
|
|
898
|
+
low = mid + 1;
|
|
899
|
+
} else {
|
|
900
|
+
high = mid;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
return low;
|
|
904
|
+
}
|
|
905
|
+
|
|
804
906
|
/**
|
|
805
907
|
* The route's own params merged with those of the mounts it sits under, in express's order: an
|
|
806
908
|
* outer mount first, the route's own last. Numbered captures do not overwrite each other, they
|
|
@@ -1712,6 +1814,90 @@ module.exports = class Router extends EventEmitter {
|
|
|
1712
1814
|
return fullMountpath;
|
|
1713
1815
|
}
|
|
1714
1816
|
|
|
1817
|
+
/**
|
|
1818
|
+
* The generic scan over this router's own table, driven by the literal index: only the routes
|
|
1819
|
+
* registered for this exact path, plus every non-literal route, are visited, in registration
|
|
1820
|
+
* order, and each visited one still answers through the same method gate and _pathMatches the
|
|
1821
|
+
* plain loop used. The routes skipped are exactly the literals whose string compare provably
|
|
1822
|
+
* fails, so the first index this answers is the one the plain loop found.
|
|
1823
|
+
*
|
|
1824
|
+
* Runs after _freezeRoutingFlags, which is what makes _caseFlag and _strictFlag readable here
|
|
1825
|
+
* and the lazily built index stable.
|
|
1826
|
+
*
|
|
1827
|
+
* @param {any} req
|
|
1828
|
+
* @param {number} startIndex where to resume the scan
|
|
1829
|
+
* @param {boolean} mayFailDecode whether the path carries a percent escape
|
|
1830
|
+
* @returns {number} the index of the route to enter, or the table length for none
|
|
1831
|
+
*/
|
|
1832
|
+
_scanFrom(req, startIndex, mayFailDecode) {
|
|
1833
|
+
const routes = this._routes;
|
|
1834
|
+
const index = (this._literalIndex ??= buildLiteralIndex(routes, /** @type {boolean} */ (this._caseFlag)));
|
|
1835
|
+
let path = req._opPath;
|
|
1836
|
+
if (path === "") {
|
|
1837
|
+
path = "/";
|
|
1838
|
+
}
|
|
1839
|
+
if (!this._caseFlag) {
|
|
1840
|
+
path = req._opPathLower ??= path.toLowerCase();
|
|
1841
|
+
}
|
|
1842
|
+
const exact = index.map.get(path) ?? EMPTY_INDICES;
|
|
1843
|
+
// the trailing-slash twin _pathMatches allows outside strict routing, as a key: a path
|
|
1844
|
+
// "/a/" can only text-match a literal "/a", so that list joins the candidates
|
|
1845
|
+
const slashed =
|
|
1846
|
+
!this._strictFlag && path.charCodeAt(path.length - 1) === 0x2f
|
|
1847
|
+
? (index.map.get(path.slice(0, -1)) ?? EMPTY_INDICES)
|
|
1848
|
+
: EMPTY_INDICES;
|
|
1849
|
+
const always = index.alwaysVisit;
|
|
1850
|
+
let exactAt = firstAtLeast(exact, startIndex);
|
|
1851
|
+
let slashedAt = firstAtLeast(slashed, startIndex);
|
|
1852
|
+
let alwaysAt = firstAtLeast(always, startIndex);
|
|
1853
|
+
const method = req.method;
|
|
1854
|
+
const none = routes.length;
|
|
1855
|
+
for (;;) {
|
|
1856
|
+
// the next candidate in registration order, from whichever list holds it
|
|
1857
|
+
let routeIndex = none;
|
|
1858
|
+
if (exactAt < exact.length && exact[exactAt] < routeIndex) {
|
|
1859
|
+
routeIndex = exact[exactAt];
|
|
1860
|
+
}
|
|
1861
|
+
if (slashedAt < slashed.length && slashed[slashedAt] < routeIndex) {
|
|
1862
|
+
routeIndex = slashed[slashedAt];
|
|
1863
|
+
}
|
|
1864
|
+
if (alwaysAt < always.length && always[alwaysAt] < routeIndex) {
|
|
1865
|
+
routeIndex = always[alwaysAt];
|
|
1866
|
+
}
|
|
1867
|
+
if (routeIndex === none) {
|
|
1868
|
+
return none;
|
|
1869
|
+
}
|
|
1870
|
+
if (exactAt < exact.length && exact[exactAt] === routeIndex) {
|
|
1871
|
+
exactAt++;
|
|
1872
|
+
}
|
|
1873
|
+
if (slashedAt < slashed.length && slashed[slashedAt] === routeIndex) {
|
|
1874
|
+
slashedAt++;
|
|
1875
|
+
}
|
|
1876
|
+
if (alwaysAt < always.length && always[alwaysAt] === routeIndex) {
|
|
1877
|
+
alwaysAt++;
|
|
1878
|
+
}
|
|
1879
|
+
const r = routes[routeIndex];
|
|
1880
|
+
// the same gates as the plain loop, comments and all: see dispatch
|
|
1881
|
+
if (!(
|
|
1882
|
+
r.all ||
|
|
1883
|
+
r.method === method ||
|
|
1884
|
+
req._isOptions ||
|
|
1885
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
1886
|
+
)) {
|
|
1887
|
+
if (mayFailDecode && this._pathMatches(r, req) && this._paramsFailToDecode(r, req)) {
|
|
1888
|
+
return routeIndex;
|
|
1889
|
+
}
|
|
1890
|
+
continue;
|
|
1891
|
+
}
|
|
1892
|
+
if (this._pathMatches(r, req)) {
|
|
1893
|
+
if (r.bodyParserOnly === true && stepsOver(r, req)) {
|
|
1894
|
+
continue;
|
|
1895
|
+
}
|
|
1896
|
+
return routeIndex;
|
|
1897
|
+
}
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
|
|
1715
1901
|
/**
|
|
1716
1902
|
* Whether a route's path matches this request. A plain string compares directly, which is what
|
|
1717
1903
|
* makes a route eligible for the native router; anything carrying a parameter or a wildcard was
|
|
@@ -1887,6 +2073,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1887
2073
|
// the "body methods" setting as it stood the first time this layer was reached,
|
|
1888
2074
|
// kept the way the parser behind it keeps it. undefined until then
|
|
1889
2075
|
bodyMethods: undefined,
|
|
2076
|
+
// a literal mount consumes exactly its registered text, so what the per-hop exec
|
|
2077
|
+
// in mountPrefixLength answers is a constant. "/" stays with the exec: its clamp
|
|
2078
|
+
// against a parent that consumed everything is not a constant
|
|
2079
|
+
mountLen:
|
|
2080
|
+
method === "USE" && typeof path === "string" && path.length > 1 && !/[:*{\\]/.test(path)
|
|
2081
|
+
? path.length
|
|
2082
|
+
: undefined,
|
|
1890
2083
|
// a mount written as a RegExp matches a piece of path that is not known until a
|
|
1891
2084
|
// request comes in, so its stack entry cannot be the path itself
|
|
1892
2085
|
regexMount: method === "USE" && path instanceof RegExp,
|
|
@@ -1937,6 +2130,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1937
2130
|
routes.push(route);
|
|
1938
2131
|
}
|
|
1939
2132
|
this._routes.push(...routes);
|
|
2133
|
+
// the literal index positions are stale the moment the table grows
|
|
2134
|
+
this._literalIndex = undefined;
|
|
1940
2135
|
|
|
1941
2136
|
// anything registered after listen invalidates what the header-skip analysis proved:
|
|
1942
2137
|
// it could catch a throw or read what a chain never did, so every skip is taken back
|
|
@@ -2396,14 +2591,23 @@ module.exports = class Router extends EventEmitter {
|
|
|
2396
2591
|
needsConversionToRegex(p) ? patternToRegex(p, false, false) : p.toLowerCase()
|
|
2397
2592
|
)
|
|
2398
2593
|
: null;
|
|
2399
|
-
const makeHandler = (chain, preset, skips) => {
|
|
2594
|
+
const makeHandler = (chain, preset, skips, wireMethod) => {
|
|
2400
2595
|
// the mutable object a granted skip lives on, so a middleware arriving after
|
|
2401
2596
|
// listen can take it back: a literal registration's preset doubles as it, and a
|
|
2402
|
-
// parameterised one, which has no preset, gets a holder of its own
|
|
2597
|
+
// parameterised one, which has no preset, gets a holder of its own. It also carries
|
|
2598
|
+
// the registration's method, so the constructor settles it with one compare
|
|
2403
2599
|
let skipHolder = preset;
|
|
2404
|
-
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery)) {
|
|
2405
|
-
skipHolder = {
|
|
2406
|
-
|
|
2600
|
+
if (skipHolder === undefined && (skips.skipHeaders || skips.skipQuery || wireMethod !== null)) {
|
|
2601
|
+
skipHolder = {
|
|
2602
|
+
skipHeaders: skips.skipHeaders,
|
|
2603
|
+
skipQuery: skips.skipQuery,
|
|
2604
|
+
method: wireMethod,
|
|
2605
|
+
isOptions: wireMethod === "OPTIONS",
|
|
2606
|
+
isHead: wireMethod === "HEAD"
|
|
2607
|
+
};
|
|
2608
|
+
if (skips.skipHeaders || skips.skipQuery) {
|
|
2609
|
+
(this._skipPresets ??= new Set()).add(skipHolder);
|
|
2610
|
+
}
|
|
2407
2611
|
}
|
|
2408
2612
|
// all three are registration-time constants: computing them in the handler was a
|
|
2409
2613
|
// closure and a scan of the chain on every native request.
|
|
@@ -2430,6 +2634,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2430
2634
|
return this._refuseRequest(response);
|
|
2431
2635
|
}
|
|
2432
2636
|
if (optimizedParams) {
|
|
2637
|
+
// slicing these out of the already-fetched path instead measured a wash:
|
|
2638
|
+
// the segment scan costs what the crossing costs
|
|
2433
2639
|
request.optimizedParams = new NullObject();
|
|
2434
2640
|
for (let i = 0; i < optimizedParams.length; i++) {
|
|
2435
2641
|
request.optimizedParams[optimizedParams[i]] = req.getParameter(i);
|
|
@@ -2475,8 +2681,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2475
2681
|
// analysis in usage.js, whose default answer is no.
|
|
2476
2682
|
//
|
|
2477
2683
|
// The etag setting is not one of the conditions. It used to be, on the grounds that send
|
|
2478
|
-
// consults freshness, but the skip branch reads if-none-match
|
|
2479
|
-
//
|
|
2684
|
+
// consults freshness, but the skip branch reads if-none-match and if-modified-since by
|
|
2685
|
+
// name whatever the setting, see the comment at request.js:527, and
|
|
2480
2686
|
// req.fresh reads nothing else off the request. Requiring etag off as well cost the copy
|
|
2481
2687
|
// to every application that left it on, which is every application that did not go
|
|
2482
2688
|
// looking for the setting.
|
|
@@ -2508,10 +2714,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
2508
2714
|
return preset;
|
|
2509
2715
|
};
|
|
2510
2716
|
|
|
2717
|
+
// the wire token this registration answers; "any" serves every verb and stays dynamic
|
|
2718
|
+
const wireMethod = method === "any" ? null : route.method;
|
|
2511
2719
|
let fn = makeHandler(
|
|
2512
2720
|
getChain,
|
|
2513
2721
|
canPreset ? makePreset(route.path, route.method, getSkips) : undefined,
|
|
2514
|
-
getSkips
|
|
2722
|
+
getSkips,
|
|
2723
|
+
wireMethod
|
|
2515
2724
|
);
|
|
2516
2725
|
const jsFn = fn;
|
|
2517
2726
|
|
|
@@ -2564,7 +2773,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
2564
2773
|
fn !== jsFn
|
|
2565
2774
|
? fn
|
|
2566
2775
|
: canPreset
|
|
2567
|
-
? makeHandler(
|
|
2776
|
+
? makeHandler(
|
|
2777
|
+
getChain,
|
|
2778
|
+
makePreset(route.path + "/", route.method, getSkips),
|
|
2779
|
+
getSkips,
|
|
2780
|
+
wireMethod
|
|
2781
|
+
)
|
|
2568
2782
|
: fn;
|
|
2569
2783
|
this.uwsApp[method](replacedPath + "/", slashFn);
|
|
2570
2784
|
if (method === "get") {
|
|
@@ -2573,7 +2787,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2573
2787
|
makeHandler(
|
|
2574
2788
|
headChain,
|
|
2575
2789
|
canPreset ? makePreset(route.path + "/", "HEAD", headSkips) : undefined,
|
|
2576
|
-
headSkips
|
|
2790
|
+
headSkips,
|
|
2791
|
+
"HEAD"
|
|
2577
2792
|
)
|
|
2578
2793
|
);
|
|
2579
2794
|
}
|
|
@@ -2582,7 +2797,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
2582
2797
|
// its own handler always: the shared one would carry the GET registration's method
|
|
2583
2798
|
this.uwsApp.head(
|
|
2584
2799
|
replacedPath,
|
|
2585
|
-
makeHandler(
|
|
2800
|
+
makeHandler(
|
|
2801
|
+
headChain,
|
|
2802
|
+
canPreset ? makePreset(route.path, "HEAD", headSkips) : undefined,
|
|
2803
|
+
headSkips,
|
|
2804
|
+
"HEAD"
|
|
2805
|
+
)
|
|
2586
2806
|
);
|
|
2587
2807
|
}
|
|
2588
2808
|
}
|
|
@@ -2741,14 +2961,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
2741
2961
|
raiseDecodeFailure(req, route, err);
|
|
2742
2962
|
return "route";
|
|
2743
2963
|
}
|
|
2744
|
-
|
|
2964
|
+
// the stack check first: it is two field loads, mergesParams is a call, and almost
|
|
2965
|
+
// no request carries a param stack at all
|
|
2966
|
+
if (req._paramStack !== null && req._paramStack.length > 0 && mergesParams(route, this)) {
|
|
2745
2967
|
req.params = mergeParams(req.params, req._paramStack);
|
|
2746
2968
|
}
|
|
2747
2969
|
} else {
|
|
2748
2970
|
// express 5 gives every matched route null-prototype params; only a pathless
|
|
2749
2971
|
// middleware layer keeps the plain object, as its router hands one to fast_slash
|
|
2750
2972
|
req.params = route.use && route.path === "" ? {} : Object.create(null);
|
|
2751
|
-
if (
|
|
2973
|
+
if (req._paramStack !== null && req._paramStack.length > 0 && mergesParams(route, this)) {
|
|
2752
2974
|
req.params = mergeParams(req.params, req._paramStack);
|
|
2753
2975
|
}
|
|
2754
2976
|
}
|
|
@@ -3132,8 +3354,12 @@ module.exports = class Router extends EventEmitter {
|
|
|
3132
3354
|
return;
|
|
3133
3355
|
}
|
|
3134
3356
|
response.status(404);
|
|
3135
|
-
// the
|
|
3136
|
-
|
|
3357
|
+
// the pathname of originalUrl, as express's finalhandler prints it: _originalPath absorbs
|
|
3358
|
+
// a req.url rewrite, originalUrl never changes
|
|
3359
|
+
const originalUrl = String(request.originalUrl);
|
|
3360
|
+
const queryIndex = originalUrl.indexOf("?");
|
|
3361
|
+
const pathname = queryIndex === -1 ? originalUrl : originalUrl.slice(0, queryIndex);
|
|
3362
|
+
this._sendErrorPage(request, response, `Cannot ${request.method} ${pathname}`, false);
|
|
3137
3363
|
}
|
|
3138
3364
|
};
|
|
3139
3365
|
|
package/src/utils.js
CHANGED
|
@@ -1510,6 +1510,11 @@ function validateHeaderName(name) {
|
|
|
1510
1510
|
}
|
|
1511
1511
|
}
|
|
1512
1512
|
|
|
1513
|
+
// values already accepted, so the constant strings middleware writes per request, helmet's CSP
|
|
1514
|
+
// among them, skip the scan. Insert-only after the regex accepts, so a hit cannot change a
|
|
1515
|
+
// verdict; bounded, and set-cookie values are per-user so they stay out
|
|
1516
|
+
const KNOWN_HEADER_VALUES = new Set();
|
|
1517
|
+
|
|
1513
1518
|
/**
|
|
1514
1519
|
* Refuses a header value holding a character that cannot go on the wire, with node's error. An
|
|
1515
1520
|
* array is sent as one header per entry, so each entry is checked on its own rather than as the
|
|
@@ -1527,9 +1532,19 @@ function validateHeaderValue(name, value) {
|
|
|
1527
1532
|
}
|
|
1528
1533
|
return;
|
|
1529
1534
|
}
|
|
1535
|
+
if (KNOWN_HEADER_VALUES.has(value)) {
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1530
1538
|
if (HEADER_VALUE.test(value)) {
|
|
1531
1539
|
throw headerError(`Invalid character in header content ["${name}"]`, "ERR_INVALID_CHAR");
|
|
1532
1540
|
}
|
|
1541
|
+
if (
|
|
1542
|
+
KNOWN_HEADER_VALUES.size < 512 &&
|
|
1543
|
+
value.length <= 1024 &&
|
|
1544
|
+
!(name.length === 10 && name.toLowerCase() === "set-cookie")
|
|
1545
|
+
) {
|
|
1546
|
+
KNOWN_HEADER_VALUES.add(value);
|
|
1547
|
+
}
|
|
1533
1548
|
}
|
|
1534
1549
|
|
|
1535
1550
|
/**
|