fulmine.js 5.12.0 → 5.12.2
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 +1 -0
- package/package.json +1 -1
- package/src/application.js +26 -23
- package/src/middlewares.js +8 -3
- package/src/request.js +71 -3
- package/src/response.js +2 -2
- package/src/router.js +165 -24
- package/src/utils.js +50 -16
package/README.md
CHANGED
|
@@ -285,6 +285,7 @@ A single-stage `node:26-trixie-slim` image works too if you `apt-get install -y
|
|
|
285
285
|
- `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).
|
|
286
286
|
- `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.
|
|
287
287
|
- 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.
|
|
288
|
+
- **A request whose `Content-Length` cannot be trusted is refused by hanging up, with no answer at all.** Node's parser refuses two of these with a `400` and Fulmine refuses the same two: a repeated `Content-Length`, whatever the values say, and one that is not a plain count of bytes, an empty value included. µWS accepts both and frames the request on the first value, or on no body at all, so what the client sent as a body is read as the next request on the connection: that is request smuggling, and a proxy in front reading the other value is all it takes. The answer differs from Express because it cannot be helped. µWS only skips the request it has already queued when the response is closed rather than completed, and writing the `400` completes it, so the choice is between telling the client and stopping the smuggled request. Nothing well behaved sends two content-lengths.
|
|
288
289
|
- **A compiled route answers `connection: keep-alive` to a client that sent `Connection: close`.** A handler simple enough to be read at registration time is answered by µWS from a response written once at `listen()`, and that response cannot read the request. The socket still closes, so what is wrong is the header and not the transport. A response that would carry a validator is never compiled, so conditional requests behave as on Express; `app.set("declarative responses", false)` turns compiling off.
|
|
289
290
|
- **Informational responses go nowhere.** `res.writeEarlyHints()`, `res.writeContinue()` and `res.writeProcessing()` are all there, take what node's take and throw what node's throw once the head has gone out, but nothing reaches the wire: µWebSockets.js has no API for a `1xx`. They exist so that code written for Express keeps running rather than dying on "is not a function", which is the only thing a drop-in can honestly promise here. `res.addTrailers()` is the same story, and `res.setTimeout()` and `req.setTimeout()` register the listener without changing anything, since µWS runs its own idle timeout through `uwsOptions.idleTimeout`.
|
|
290
291
|
- For HTTPS, instead of doing this:
|
package/package.json
CHANGED
package/src/application.js
CHANGED
|
@@ -208,11 +208,11 @@ class Application extends Router {
|
|
|
208
208
|
// a "trust proxy" this app never set is inherited from the parent, as express does:
|
|
209
209
|
// the defaults are deleted so get() falls through to the parent's value
|
|
210
210
|
if (
|
|
211
|
-
this.
|
|
212
|
-
typeof parent.
|
|
211
|
+
this._settings[trustProxyDefaultSymbol] === true &&
|
|
212
|
+
typeof parent._settings["trust proxy fn"] === "function"
|
|
213
213
|
) {
|
|
214
|
-
delete this.
|
|
215
|
-
delete this.
|
|
214
|
+
delete this._settings["trust proxy"];
|
|
215
|
+
delete this._settings["trust proxy fn"];
|
|
216
216
|
}
|
|
217
217
|
});
|
|
218
218
|
this.listenCalled = false;
|
|
@@ -248,20 +248,20 @@ class Application extends Router {
|
|
|
248
248
|
this._draining = false;
|
|
249
249
|
// read here, at construction, the way express does; an empty NODE_ENV means development,
|
|
250
250
|
// which the ?? in the shared default would miss
|
|
251
|
-
if (typeof this.
|
|
252
|
-
this.
|
|
251
|
+
if (typeof this._settings.env === "undefined") {
|
|
252
|
+
this._settings.env = process.env.NODE_ENV || "development";
|
|
253
253
|
}
|
|
254
254
|
for (const key in defaultSettings) {
|
|
255
|
-
if (typeof this.
|
|
255
|
+
if (typeof this._settings[key] === "undefined") {
|
|
256
256
|
if (typeof defaultSettings[key] === "function") {
|
|
257
|
-
this.
|
|
257
|
+
this._settings[key] = defaultSettings[key](this);
|
|
258
258
|
} else {
|
|
259
|
-
this.
|
|
259
|
+
this._settings[key] = defaultSettings[key];
|
|
260
260
|
}
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
// non-enumerable, so the marker never shows up walking the settings
|
|
264
|
-
Object.defineProperty(this.
|
|
264
|
+
Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
|
|
265
265
|
configurable: true,
|
|
266
266
|
value: true
|
|
267
267
|
});
|
|
@@ -372,27 +372,27 @@ class Application extends Router {
|
|
|
372
372
|
if (!value) {
|
|
373
373
|
// compiled, not deleted: an explicit false must shadow a parent's setting when
|
|
374
374
|
// this app is mounted, and a deleted key would read straight through to it
|
|
375
|
-
this.
|
|
375
|
+
this._settings["trust proxy fn"] = compileTrust(false);
|
|
376
376
|
} else {
|
|
377
|
-
this.
|
|
377
|
+
this._settings["trust proxy fn"] = compileTrust(value);
|
|
378
378
|
}
|
|
379
379
|
// set explicitly, so a mount no longer inherits the parent's
|
|
380
|
-
Object.defineProperty(this.
|
|
380
|
+
Object.defineProperty(this._settings, trustProxyDefaultSymbol, {
|
|
381
381
|
configurable: true,
|
|
382
382
|
value: false
|
|
383
383
|
});
|
|
384
384
|
} else if (key === "stat cache") {
|
|
385
385
|
// compiled here so the read path is a number and not a duration to parse per request
|
|
386
|
-
this.
|
|
386
|
+
this._settings["stat cache ms"] = durationSetting(value, "stat cache");
|
|
387
387
|
} else if (key === "query parser") {
|
|
388
388
|
if (value === "extended") {
|
|
389
|
-
this.
|
|
389
|
+
this._settings["query parser fn"] = fastQueryParse;
|
|
390
390
|
} else if (value === "simple" || value === true) {
|
|
391
|
-
this.
|
|
391
|
+
this._settings["query parser fn"] = parseQuery;
|
|
392
392
|
} else if (typeof value === "function") {
|
|
393
|
-
this.
|
|
393
|
+
this._settings["query parser fn"] = value;
|
|
394
394
|
} else if (value === false) {
|
|
395
|
-
this.
|
|
395
|
+
this._settings["query parser fn"] = undefined;
|
|
396
396
|
} else {
|
|
397
397
|
// express's wording, which applications match on
|
|
398
398
|
throw new TypeError("unknown value for query parser function: " + value);
|
|
@@ -414,18 +414,18 @@ class Application extends Router {
|
|
|
414
414
|
// request. Registering a route or a middleware after listen still takes them back,
|
|
415
415
|
// see router.js:1615: that is a different question, about code the analysis never saw.
|
|
416
416
|
if (typeof value === "function") {
|
|
417
|
-
this.
|
|
417
|
+
this._settings["etag fn"] = value;
|
|
418
418
|
} else {
|
|
419
419
|
switch (value) {
|
|
420
420
|
case true:
|
|
421
421
|
case "weak":
|
|
422
|
-
this.
|
|
422
|
+
this._settings["etag fn"] = createETagGenerator({ weak: true });
|
|
423
423
|
break;
|
|
424
424
|
case "strong":
|
|
425
|
-
this.
|
|
425
|
+
this._settings["etag fn"] = createETagGenerator({ weak: false });
|
|
426
426
|
break;
|
|
427
427
|
case false:
|
|
428
|
-
delete this.
|
|
428
|
+
delete this._settings["etag fn"];
|
|
429
429
|
break;
|
|
430
430
|
default:
|
|
431
431
|
// express's wording, which applications match on
|
|
@@ -434,7 +434,7 @@ class Application extends Router {
|
|
|
434
434
|
}
|
|
435
435
|
}
|
|
436
436
|
|
|
437
|
-
this.
|
|
437
|
+
this._settings[key] = value;
|
|
438
438
|
// any app's hot-settings copy may resolve through this one, see Router#_hot
|
|
439
439
|
settingsEpoch.n++;
|
|
440
440
|
return this;
|
|
@@ -529,6 +529,9 @@ class Application extends Router {
|
|
|
529
529
|
_serveGeneric(res, req) {
|
|
530
530
|
const request = this.handleRequest(res, req);
|
|
531
531
|
const response = request.res;
|
|
532
|
+
if (request._badFraming === true) {
|
|
533
|
+
return this._refuseFraming(response);
|
|
534
|
+
}
|
|
532
535
|
try {
|
|
533
536
|
this._routeRequestDirect(request, response);
|
|
534
537
|
} finally {
|
package/src/middlewares.js
CHANGED
|
@@ -538,7 +538,7 @@ function serveStatic(root, options) {
|
|
|
538
538
|
filePath,
|
|
539
539
|
req.headers["accept-encoding"],
|
|
540
540
|
twinTtl,
|
|
541
|
-
req.app.
|
|
541
|
+
req.app._settings["stat cache ms"]
|
|
542
542
|
);
|
|
543
543
|
if (twin) {
|
|
544
544
|
stat = twin.stat;
|
|
@@ -546,7 +546,7 @@ function serveStatic(root, options) {
|
|
|
546
546
|
}
|
|
547
547
|
try {
|
|
548
548
|
if (stat === undefined) {
|
|
549
|
-
stat = cachedStat(statTarget, req.app.
|
|
549
|
+
stat = cachedStat(statTarget, req.app._settings["stat cache ms"]);
|
|
550
550
|
}
|
|
551
551
|
} catch (err) {
|
|
552
552
|
// the one to report when nothing is found: send hands each failed attempt to the next
|
|
@@ -656,7 +656,12 @@ function serveStatic(root, options) {
|
|
|
656
656
|
// already found before the stat below, on the ordinary path
|
|
657
657
|
const variant =
|
|
658
658
|
twin ??
|
|
659
|
-
pickPrecompressed(
|
|
659
|
+
pickPrecompressed(
|
|
660
|
+
filePath,
|
|
661
|
+
req.headers["accept-encoding"],
|
|
662
|
+
twinTtl,
|
|
663
|
+
req.app._settings["stat cache ms"]
|
|
664
|
+
);
|
|
660
665
|
if (variant) {
|
|
661
666
|
_path += variant.suffix;
|
|
662
667
|
stat = variant.stat;
|
package/src/request.js
CHANGED
|
@@ -152,6 +152,30 @@ const discardedDuplicates = new Set([
|
|
|
152
152
|
// 128 KB of body buffered before uWS is asked to pause
|
|
153
153
|
const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
154
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
157
|
+
*
|
|
158
|
+
* uWS trims the spaces around the value and then takes whatever is left, so "", "abc", "+1", "-1",
|
|
159
|
+
* "0x10" and "1e2" all arrive here. Every one of them makes uWS frame the request as carrying no
|
|
160
|
+
* body, and what the client sent as a body is then read as the next request on the connection.
|
|
161
|
+
* Node's parser refuses all of them outright, and so does this.
|
|
162
|
+
*
|
|
163
|
+
* @param {string} value as uWS hands it over
|
|
164
|
+
* @returns {boolean}
|
|
165
|
+
*/
|
|
166
|
+
function isByteCount(value) {
|
|
167
|
+
if (value.length === 0) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
for (let i = 0; i < value.length; i++) {
|
|
171
|
+
const code = value.charCodeAt(i);
|
|
172
|
+
if (code < 0x30 || code > 0x39) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
|
|
155
179
|
// Whose headers the shared collector below is filling. uWS's forEach is synchronous and runs no
|
|
156
180
|
// user code, so the handoff cannot interleave; module-level so the callback exists once instead
|
|
157
181
|
// of once per request.
|
|
@@ -336,6 +360,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
336
360
|
(headerKey.length === 14 && headerKey === "content-length") ||
|
|
337
361
|
(headerKey.length === 17 && headerKey === "transfer-encoding")
|
|
338
362
|
) {
|
|
363
|
+
if (headerKey.length === 14) {
|
|
364
|
+
// a second content-length whatever it says, and one that is not a count of bytes:
|
|
365
|
+
// both make uWS frame the request differently from what is on the wire, see
|
|
366
|
+
// _badFraming and isByteCount
|
|
367
|
+
if (r._sawContentLength || !isByteCount(value)) {
|
|
368
|
+
r._badFraming = true;
|
|
369
|
+
}
|
|
370
|
+
r._sawContentLength = true;
|
|
371
|
+
}
|
|
339
372
|
// saying anything about framing at all, "0" included. A parser that can see a
|
|
340
373
|
// content-length answers about the body it describes, even an empty one: a zero length
|
|
341
374
|
// with a charset nobody can decode is a 415 in express and here, so a chain may only
|
|
@@ -439,6 +472,33 @@ module.exports = class Request extends LazyReadable {
|
|
|
439
472
|
*/
|
|
440
473
|
_hasBodyHeaders;
|
|
441
474
|
|
|
475
|
+
/**
|
|
476
|
+
* Whether a content-length has already been copied, so a second one is spotted. Declared for
|
|
477
|
+
* the same reason as rawIp.
|
|
478
|
+
* @type {boolean|undefined}
|
|
479
|
+
*/
|
|
480
|
+
_sawContentLength;
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Whether the request said two different things about how long its body is, so uWS may have
|
|
484
|
+
* framed it differently from the client that sent it and the proxy that forwarded it. Two
|
|
485
|
+
* shapes reach this, and node's parser refuses both outright:
|
|
486
|
+
*
|
|
487
|
+
* a repeated content-length uWS frames on the first and drops the rest, so a proxy in
|
|
488
|
+
* front reading the last one instead forwards bytes uWS then
|
|
489
|
+
* answers as a second, pipelined request
|
|
490
|
+
* one that is not a byte count uWS keeps whatever is left after trimming, an empty value
|
|
491
|
+
* included, and frames the request as carrying no body at all,
|
|
492
|
+
* which turns the body the client sent into that same second
|
|
493
|
+
* request. See isByteCount
|
|
494
|
+
*
|
|
495
|
+
* Either way it is request smuggling, and the request is refused rather than routed. Declared
|
|
496
|
+
* for the same reason as rawIp.
|
|
497
|
+
*
|
|
498
|
+
* @type {boolean|undefined}
|
|
499
|
+
*/
|
|
500
|
+
_badFraming;
|
|
501
|
+
|
|
442
502
|
/**
|
|
443
503
|
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
444
504
|
* @type {boolean|undefined}
|
|
@@ -504,10 +564,18 @@ module.exports = class Request extends LazyReadable {
|
|
|
504
564
|
// which is 0.2us on a request that is about to read a body.
|
|
505
565
|
const length = req.getHeader("content-length");
|
|
506
566
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
567
|
+
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
568
|
+
// getHeader only ever returns the first of a repeated header, so a duplicate cannot be
|
|
569
|
+
// seen from here, and a duplicate has to be refused rather than routed: see
|
|
570
|
+
// _badFraming. Anything that says a word about framing takes the full copy instead.
|
|
571
|
+
//
|
|
572
|
+
// One shape stays invisible here, a content-length present with an empty value: uWS
|
|
573
|
+
// answers "" for that and for a header that was never sent, and nothing in its API
|
|
574
|
+
// tells them apart. It frames both as carrying no body, which is the right reading of
|
|
575
|
+
// the second, so this server stays consistent with itself either way. The full copy
|
|
576
|
+
// below does refuse it, which is every request except a GET whose whole chain provably
|
|
577
|
+
// reads no header at all.
|
|
507
578
|
if (length !== "" || transferEncoding !== "") {
|
|
508
|
-
this._hasBodyHeaders = true;
|
|
509
|
-
}
|
|
510
|
-
if ((length !== "" && length !== "0") || transferEncoding !== "") {
|
|
511
579
|
currentRequest = this;
|
|
512
580
|
this._req.forEach(Request.#collectHeader);
|
|
513
581
|
currentRequest = null;
|
package/src/response.js
CHANGED
|
@@ -296,7 +296,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
296
296
|
// "connection headers" off advertises neither, which Express always does: see below for
|
|
297
297
|
// the one this still writes.
|
|
298
298
|
this.headers =
|
|
299
|
-
res._nodeRes || app.
|
|
299
|
+
res._nodeRes || app._settings["connection headers"] === false
|
|
300
300
|
? {}
|
|
301
301
|
: {
|
|
302
302
|
connection: "keep-alive",
|
|
@@ -1066,7 +1066,7 @@ module.exports = class Response extends LazyWritable {
|
|
|
1066
1066
|
let stat = options._stat;
|
|
1067
1067
|
if (!stat) {
|
|
1068
1068
|
try {
|
|
1069
|
-
stat = cachedStat(fullpath, this.app.
|
|
1069
|
+
stat = cachedStat(fullpath, this.app._settings["stat cache ms"]);
|
|
1070
1070
|
} catch (err) {
|
|
1071
1071
|
// the fs error itself, carrying its errno and path, with send's status written on
|
|
1072
1072
|
// it: a missing file is the request's 404, an unreadable one is the server's 500
|
package/src/router.js
CHANGED
|
@@ -178,7 +178,10 @@ class Walk {
|
|
|
178
178
|
// inside the route. req.next itself is left alone: making it mean this everywhere is what
|
|
179
179
|
// express does, and it breaks express own res.format and app.routes.error tests here, so
|
|
180
180
|
// that stays open rather than half done.
|
|
181
|
-
|
|
181
|
+
//
|
|
182
|
+
// Null here and bound on the first route that has more than one callback, which is the
|
|
183
|
+
// only shape that ever reads it: a request that never meets one paid a bind for nothing
|
|
184
|
+
this.leaveRoute = null;
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
/**
|
|
@@ -238,7 +241,18 @@ class Walk {
|
|
|
238
241
|
// was one closure per hop of every request not on a compiled chain
|
|
239
242
|
for (; routeIndex < routes.length; routeIndex++) {
|
|
240
243
|
const r = routes[routeIndex];
|
|
241
|
-
|
|
244
|
+
// A HEAD request enters a route whose path matched even when its verb cannot serve
|
|
245
|
+
// one: express exempts HEAD from the method check ("if (!hasMethod && method !==
|
|
246
|
+
// 'HEAD')" in router/index.js), so the layer's parameters are captured and its
|
|
247
|
+
// param() callbacks run before the route is dropped. Only asked when the router has
|
|
248
|
+
// callbacks to run, since entering a route to step straight back out of it is
|
|
249
|
+
// otherwise pure cost with nothing to show for it. runRoute steps over it.
|
|
250
|
+
if (!(
|
|
251
|
+
r.all ||
|
|
252
|
+
r.method === req.method ||
|
|
253
|
+
req._isOptions ||
|
|
254
|
+
(req._isHead && (r.gettable || r.paramCallbacks.size > 0))
|
|
255
|
+
)) {
|
|
242
256
|
// taken only to fail: _preprocessRequest decodes again and turns it into the
|
|
243
257
|
// error, so the handlers of a route this request cannot run never see it
|
|
244
258
|
if (mayFailDecode && router._pathMatches(r, req) && router._paramsFailToDecode(r, req)) {
|
|
@@ -362,7 +376,7 @@ class Walk {
|
|
|
362
376
|
// express hands res.format's handlers the next its own layer received, and its test asserts
|
|
363
377
|
// that identity. With more than one callback the two differ for real, and what express
|
|
364
378
|
// hands over is the one that leaves the route
|
|
365
|
-
req._leaveRoute = route.callbacks.length > 1 ? this.leaveRoute : this.next;
|
|
379
|
+
req._leaveRoute = route.callbacks.length > 1 ? (this.leaveRoute ??= this.stepOutOfRoute.bind(this)) : this.next;
|
|
366
380
|
if (continueRoute === "route") {
|
|
367
381
|
this.step("route");
|
|
368
382
|
} else if (continueRoute) {
|
|
@@ -475,7 +489,7 @@ class Walk {
|
|
|
475
489
|
rememberApp(this, route, req);
|
|
476
490
|
useApp(req, callback);
|
|
477
491
|
}
|
|
478
|
-
const pushedParams = callback.
|
|
492
|
+
const pushedParams = callback._settings.mergeParams;
|
|
479
493
|
if (pushedParams) {
|
|
480
494
|
(req._paramStack ??= []).push(req.params);
|
|
481
495
|
}
|
|
@@ -511,20 +525,20 @@ class Walk {
|
|
|
511
525
|
if (parentMethods !== null) {
|
|
512
526
|
req._matchedMethods = parentMethods;
|
|
513
527
|
}
|
|
514
|
-
if (req._isOptions && childMethods.size) {
|
|
528
|
+
if (req._isOptions && childMethods.size && !req._error) {
|
|
515
529
|
// OPTIONS routing is different, it stops in the router if matched.
|
|
516
530
|
// Express answers as the router hands back, so a throw while answering,
|
|
517
531
|
// a head already written being the way, walks on to later error handlers
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
return this.step(err);
|
|
524
|
-
}
|
|
532
|
+
try {
|
|
533
|
+
router._sendOptionsReply(req, res, childMethods);
|
|
534
|
+
return this.resolve(true);
|
|
535
|
+
} catch (err) {
|
|
536
|
+
return this.step(err);
|
|
525
537
|
}
|
|
526
|
-
return this.resolve(false);
|
|
527
538
|
}
|
|
539
|
+
// An error carried out of the mount is not answered by the automatic reply, and
|
|
540
|
+
// stopping here handed it to the default page: express walks on to the error
|
|
541
|
+
// handlers written after the mount, for OPTIONS as for any other method.
|
|
528
542
|
this.step(undefined);
|
|
529
543
|
})
|
|
530
544
|
// a rejection out of the nested walk, or a throw above, must reject this one
|
|
@@ -547,6 +561,11 @@ class Walk {
|
|
|
547
561
|
}
|
|
548
562
|
return this.step(undefined);
|
|
549
563
|
}
|
|
564
|
+
// entered only so its param callbacks could run, see the scan in dispatch: the verb
|
|
565
|
+
// cannot serve a HEAD, so nothing here answers it
|
|
566
|
+
if (req._isHead && !route.all && !route.gettable && route.method !== "HEAD") {
|
|
567
|
+
return this.step(undefined);
|
|
568
|
+
}
|
|
550
569
|
|
|
551
570
|
const out = callback(req, res, this.next);
|
|
552
571
|
if (out instanceof Promise) {
|
|
@@ -668,6 +687,42 @@ function setMountedPath(req) {
|
|
|
668
687
|
req._lastUrl = req.url;
|
|
669
688
|
}
|
|
670
689
|
|
|
690
|
+
const NO_PARAM_NAMES = [];
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* The parameter names a route captures with its own pattern.
|
|
694
|
+
*
|
|
695
|
+
* This is the set express runs param callbacks for. A name that reached req.params from a mount
|
|
696
|
+
* above, through mergeParams, belongs to that mount's router and not to this one, and express does
|
|
697
|
+
* not call this router's param() for it: it walks the keys the layer itself matched. Reading
|
|
698
|
+
* req.params instead ran a callback for every inherited name too, which is visible whenever such a
|
|
699
|
+
* callback does anything, and turned a 200 into a 500 when one of them refused the value.
|
|
700
|
+
*
|
|
701
|
+
* Worked out once per route and kept, since it follows from the pattern and never changes.
|
|
702
|
+
*
|
|
703
|
+
* @param {any} route
|
|
704
|
+
* @returns {string[]}
|
|
705
|
+
*/
|
|
706
|
+
function ownParamNames(route) {
|
|
707
|
+
let names = route._ownParamNames;
|
|
708
|
+
if (names !== undefined) {
|
|
709
|
+
return names;
|
|
710
|
+
}
|
|
711
|
+
if (route.optimizedParams) {
|
|
712
|
+
// µWS matched the pattern and hands the values back by position, under these names
|
|
713
|
+
names = route.optimizedParams;
|
|
714
|
+
} else if (route.pattern instanceof RegExp) {
|
|
715
|
+
const meta = getPatternMeta(route.pattern);
|
|
716
|
+
// outputNames is what _extractParams writes into params; a RegExp the application wrote
|
|
717
|
+
// itself was never compiled here, so its capture groups are the names
|
|
718
|
+
names = meta ? meta.outputNames : regexpGroupKeys(route.pattern);
|
|
719
|
+
} else {
|
|
720
|
+
names = NO_PARAM_NAMES;
|
|
721
|
+
}
|
|
722
|
+
route._ownParamNames = names;
|
|
723
|
+
return names;
|
|
724
|
+
}
|
|
725
|
+
|
|
671
726
|
/**
|
|
672
727
|
* Whether this route reads the parameters of the mounts above it, which is its own router asking
|
|
673
728
|
* for them. The stack holds what a mergeParams router captured on the way in, and a plain router
|
|
@@ -679,7 +734,7 @@ function setMountedPath(req) {
|
|
|
679
734
|
*/
|
|
680
735
|
function mergesParams(route, fallback) {
|
|
681
736
|
const owner = route.owner ?? fallback;
|
|
682
|
-
return Boolean(owner?.
|
|
737
|
+
return Boolean(owner?._settings?.mergeParams);
|
|
683
738
|
}
|
|
684
739
|
|
|
685
740
|
/**
|
|
@@ -819,6 +874,46 @@ function onNativeAborted() {
|
|
|
819
874
|
response.socket?.emit("error", err);
|
|
820
875
|
}
|
|
821
876
|
|
|
877
|
+
/**
|
|
878
|
+
* What app.settings is wrapped in, so a write that never went through set() still tells the hot
|
|
879
|
+
* copies they are out of date. Only writes are trapped: a missing trap is the plain operation on
|
|
880
|
+
* the object itself, so reads through here behave exactly as they did.
|
|
881
|
+
*
|
|
882
|
+
* defineProperty is here for the trust proxy default marker, which set() writes that way, and for
|
|
883
|
+
* anything else reaching for Object.defineProperty rather than an assignment.
|
|
884
|
+
*/
|
|
885
|
+
const settingsWriteTraps = {
|
|
886
|
+
/**
|
|
887
|
+
* @param {any} target
|
|
888
|
+
* @param {string|symbol} key
|
|
889
|
+
* @param {any} value
|
|
890
|
+
*/
|
|
891
|
+
set(target, key, value) {
|
|
892
|
+
target[key] = value;
|
|
893
|
+
settingsEpoch.n++;
|
|
894
|
+
return true;
|
|
895
|
+
},
|
|
896
|
+
/**
|
|
897
|
+
* @param {any} target
|
|
898
|
+
* @param {string|symbol} key
|
|
899
|
+
*/
|
|
900
|
+
deleteProperty(target, key) {
|
|
901
|
+
delete target[key];
|
|
902
|
+
settingsEpoch.n++;
|
|
903
|
+
return true;
|
|
904
|
+
},
|
|
905
|
+
/**
|
|
906
|
+
* @param {any} target
|
|
907
|
+
* @param {string|symbol} key
|
|
908
|
+
* @param {any} descriptor
|
|
909
|
+
*/
|
|
910
|
+
defineProperty(target, key, descriptor) {
|
|
911
|
+
Object.defineProperty(target, key, descriptor);
|
|
912
|
+
settingsEpoch.n++;
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
|
|
822
917
|
/**
|
|
823
918
|
* The per-request constants of a fully literal native registration. µWS matched the URL byte for
|
|
824
919
|
* byte against this exact pattern and dispatches by method, so the request constructor can take
|
|
@@ -1294,19 +1389,26 @@ module.exports = class Router extends EventEmitter {
|
|
|
1294
1389
|
// an array when mounted on several paths at once, as Express allows
|
|
1295
1390
|
/** @type {string|string[]} */
|
|
1296
1391
|
this.mountpath = "/";
|
|
1297
|
-
|
|
1392
|
+
// The settings twice: the plain object everything inside here reads, and the Proxy the
|
|
1393
|
+
// outside gets. Express lets an application write app.settings["x"] straight, which set()
|
|
1394
|
+
// never sees, so the hot copies in _hot() would keep answering the old value until an
|
|
1395
|
+
// unrelated set() happened to bump the epoch and the change landed by surprise. The trap
|
|
1396
|
+
// bumps it on the spot. Reading through a Proxy costs about 20ns, which is why the inside
|
|
1397
|
+
// never does: _settings is the same object without the wrapper.
|
|
1398
|
+
this._settings = settings;
|
|
1399
|
+
this.settings = new Proxy(settings, settingsWriteTraps);
|
|
1298
1400
|
// the base classes; an Application replaces these with its own per-app subclasses, and a
|
|
1299
1401
|
// plain router has no request/response prototype layer, as in Express
|
|
1300
1402
|
this._request = Request;
|
|
1301
1403
|
this._response = Response;
|
|
1302
1404
|
|
|
1303
1405
|
if (typeof settings.caseSensitive !== "undefined") {
|
|
1304
|
-
|
|
1305
|
-
delete
|
|
1406
|
+
settings["case sensitive routing"] = settings.caseSensitive;
|
|
1407
|
+
delete settings.caseSensitive;
|
|
1306
1408
|
}
|
|
1307
1409
|
if (typeof settings.strict !== "undefined") {
|
|
1308
|
-
|
|
1309
|
-
delete
|
|
1410
|
+
settings["strict routing"] = settings.strict;
|
|
1411
|
+
delete settings.strict;
|
|
1310
1412
|
}
|
|
1311
1413
|
}
|
|
1312
1414
|
|
|
@@ -1393,7 +1495,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
1393
1495
|
get(path, ...callbacks) {
|
|
1394
1496
|
if (typeof path === "string" && callbacks.length === 0) {
|
|
1395
1497
|
const key = path;
|
|
1396
|
-
|
|
1498
|
+
// the raw object, not the Proxy: see the constructor
|
|
1499
|
+
const res = this._settings[key];
|
|
1397
1500
|
if (typeof res === "undefined" && this.parent) {
|
|
1398
1501
|
return this.parent.get(key);
|
|
1399
1502
|
} else {
|
|
@@ -1441,7 +1544,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1441
1544
|
* @returns {boolean}
|
|
1442
1545
|
*/
|
|
1443
1546
|
_routingFlag(key) {
|
|
1444
|
-
const own = this.
|
|
1547
|
+
const own = this._settings[key];
|
|
1445
1548
|
if (typeof own !== "undefined") {
|
|
1446
1549
|
return Boolean(own);
|
|
1447
1550
|
}
|
|
@@ -2014,6 +2117,30 @@ module.exports = class Router extends EventEmitter {
|
|
|
2014
2117
|
return request;
|
|
2015
2118
|
}
|
|
2016
2119
|
|
|
2120
|
+
/**
|
|
2121
|
+
* Refuses a request whose framing cannot be trusted and hangs up without answering. No route
|
|
2122
|
+
* runs, so nothing downstream can be reached by one.
|
|
2123
|
+
*
|
|
2124
|
+
* Hanging up is the whole point: uWS has already read what followed the body it believed in as
|
|
2125
|
+
* a second, pipelined request, and it dispatches that one unless the socket goes. Node answers
|
|
2126
|
+
* 400 and then closes, and this cannot do both: uWS only skips the queued request when the
|
|
2127
|
+
* response is closed rather than completed, and any of writeStatus, end or endWithoutBody
|
|
2128
|
+
* completes it. Measured every combination, and delivering the 400 always let the smuggled
|
|
2129
|
+
* request through, so the close wins and the client gets nothing. Nothing legitimate sends two
|
|
2130
|
+
* content-lengths, so there is no well-behaved client to explain it to.
|
|
2131
|
+
*
|
|
2132
|
+
* Called once handleRequest has fully returned, never from inside it: an Application links the
|
|
2133
|
+
* response into its pending list after the base call, and the 'close' emitted here is what
|
|
2134
|
+
* takes it back out again.
|
|
2135
|
+
*
|
|
2136
|
+
* @param {any} response
|
|
2137
|
+
*/
|
|
2138
|
+
_refuseFraming(response) {
|
|
2139
|
+
response.finished = true;
|
|
2140
|
+
response._res.close();
|
|
2141
|
+
response.emit("close");
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2017
2144
|
/**
|
|
2018
2145
|
* Tells uWS whom to call on a client abort. Out of handleRequest, because uWS only needs it
|
|
2019
2146
|
* for a response that outlives its handler callback: the native handler arms it in its
|
|
@@ -2120,6 +2247,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
2120
2247
|
}
|
|
2121
2248
|
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
2122
2249
|
const response = request.res;
|
|
2250
|
+
if (request._badFraming === true) {
|
|
2251
|
+
return this._refuseFraming(response);
|
|
2252
|
+
}
|
|
2123
2253
|
if (optimizedParams) {
|
|
2124
2254
|
request.optimizedParams = new NullObject();
|
|
2125
2255
|
for (let i = 0; i < optimizedParams.length; i++) {
|
|
@@ -2436,8 +2566,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
2436
2566
|
|
|
2437
2567
|
// the route's own router's callbacks: an optimized chain is walked by the app even when it
|
|
2438
2568
|
// ends in a mounted router's route
|
|
2569
|
+
//
|
|
2570
|
+
// A route an OPTIONS request reaches only to have its verb counted for the automatic reply
|
|
2571
|
+
// is not a route this request runs, and express does not run its app.param() callbacks for
|
|
2572
|
+
// it. The same condition runRoute counts the verb under, see the OPTIONS branch there. The
|
|
2573
|
+
// decoding above still happens either way, because express decodes a layer whose path
|
|
2574
|
+
// matched whatever its method is, which is what answers 400 for a malformed escape.
|
|
2439
2575
|
const paramCallbacks = route.paramCallbacks;
|
|
2440
|
-
if (paramCallbacks.size > 0) {
|
|
2576
|
+
if (paramCallbacks.size > 0 && !(req._isOptions && !route.all && route.method !== "OPTIONS")) {
|
|
2441
2577
|
return this._runParamCallbacks(req, res, route, paramCallbacks);
|
|
2442
2578
|
}
|
|
2443
2579
|
return true;
|
|
@@ -2480,9 +2616,14 @@ module.exports = class Router extends EventEmitter {
|
|
|
2480
2616
|
* @returns {Promise<true|"route">|true}
|
|
2481
2617
|
*/
|
|
2482
2618
|
_runParamCallbacks(req, res, route, paramCallbacks) {
|
|
2619
|
+
// the names this route captured itself, not everything in req.params: a merged-in name
|
|
2620
|
+
// belongs to the mount that captured it, see ownParamNames
|
|
2483
2621
|
let names;
|
|
2484
|
-
|
|
2485
|
-
|
|
2622
|
+
const own = ownParamNames(route);
|
|
2623
|
+
for (let i = 0; i < own.length; i++) {
|
|
2624
|
+
const name = own[i];
|
|
2625
|
+
// an optional group that did not match leaves no parameter to call anything for
|
|
2626
|
+
if (paramCallbacks.has(name) && req.params[name] !== undefined) {
|
|
2486
2627
|
(names ??= []).push(name);
|
|
2487
2628
|
}
|
|
2488
2629
|
}
|
package/src/utils.js
CHANGED
|
@@ -596,13 +596,23 @@ function canBeOptimizedWithParams(pattern) {
|
|
|
596
596
|
return true;
|
|
597
597
|
}
|
|
598
598
|
|
|
599
|
+
// What makes a segment something other than the text it is written as: a parameter, a wildcard, an
|
|
600
|
+
// optional group, or an escape. Only two plain literals can prove that two paths never meet, so
|
|
601
|
+
// anything carrying one of these has to be read as "could be anything".
|
|
602
|
+
const NOT_A_LITERAL = /[:*{}\\]/;
|
|
603
|
+
|
|
599
604
|
/**
|
|
600
605
|
* Whether two paths could both match the same request.
|
|
601
606
|
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
*
|
|
607
|
+
* The answer is structural: no position where two different literals meet, and, when neither path
|
|
608
|
+
* can change length, the same number of segments. `/orders/:id` and `/invoices/:id` cannot both
|
|
609
|
+
* match, `/users/:id` and `/users/me` can. The caller reads "do not know" as yes, so every doubt
|
|
610
|
+
* answers true: saying two paths overlap only costs a native registration, while missing one lets
|
|
611
|
+
* µWS answer a request that belonged to an earlier route.
|
|
612
|
+
*
|
|
613
|
+
* A parameter is not the only shape that matches more than itself. A wildcard and an optional group
|
|
614
|
+
* do too, and reading `{:opt}` or `*splat` as the literal text it is written as reported "cannot
|
|
615
|
+
* overlap" for a route that plainly could, which took the earlier route's turn away.
|
|
606
616
|
*
|
|
607
617
|
* @param {string} a
|
|
608
618
|
* @param {string} b
|
|
@@ -612,15 +622,19 @@ function canBeOptimizedWithParams(pattern) {
|
|
|
612
622
|
function pathsCanOverlap(a, b, aIsPrefix = false) {
|
|
613
623
|
const left = a.split("/");
|
|
614
624
|
const right = b.split("/");
|
|
615
|
-
|
|
625
|
+
// An optional group matches its segment or nothing at all and a wildcard matches several, so a
|
|
626
|
+
// path carrying either one matches more than one length and the count settles nothing.
|
|
627
|
+
const fixedLength =
|
|
628
|
+
a.indexOf("{") === -1 && b.indexOf("{") === -1 && a.indexOf("*") === -1 && b.indexOf("*") === -1;
|
|
629
|
+
if (fixedLength && (aIsPrefix ? left.length > right.length : left.length !== right.length)) {
|
|
616
630
|
return false;
|
|
617
631
|
}
|
|
618
|
-
|
|
632
|
+
const shared = left.length < right.length ? left.length : right.length;
|
|
633
|
+
for (let i = 0; i < shared; i++) {
|
|
619
634
|
if (left[i] === right[i]) {
|
|
620
635
|
continue;
|
|
621
636
|
}
|
|
622
|
-
|
|
623
|
-
if (left[i].charCodeAt(0) === 0x3a || right[i].charCodeAt(0) === 0x3a) {
|
|
637
|
+
if (NOT_A_LITERAL.test(left[i]) || NOT_A_LITERAL.test(right[i])) {
|
|
624
638
|
continue;
|
|
625
639
|
}
|
|
626
640
|
return false;
|
|
@@ -1436,6 +1450,32 @@ function withUtf8Charset(value) {
|
|
|
1436
1450
|
const HEADER_TOKEN = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/;
|
|
1437
1451
|
const HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
|
|
1438
1452
|
|
|
1453
|
+
/**
|
|
1454
|
+
* One of node's header errors, built the way node builds it.
|
|
1455
|
+
*
|
|
1456
|
+
* Assigning the code is not the whole of it. Node also puts the code in the first line of the
|
|
1457
|
+
* stack, by naming the error "TypeError [THE_CODE]" while V8 formats that line and then taking
|
|
1458
|
+
* the name back off. Whatever prints a stack therefore says which code it was, and the default
|
|
1459
|
+
* error page prints exactly that: without this, the same refusal reads "TypeError:" here and
|
|
1460
|
+
* "TypeError [ERR_INVALID_CHAR]:" behind Express. Found by fuzzing against express.
|
|
1461
|
+
*
|
|
1462
|
+
* @param {string} message
|
|
1463
|
+
* @param {string} code
|
|
1464
|
+
* @returns {NodeJS.ErrnoException}
|
|
1465
|
+
*/
|
|
1466
|
+
function headerError(message, code) {
|
|
1467
|
+
/** @type {NodeJS.ErrnoException} */
|
|
1468
|
+
const err = new TypeError(message);
|
|
1469
|
+
err.name = `TypeError [${code}]`;
|
|
1470
|
+
// reading it is what makes V8 format the line, and it formats it from the name above
|
|
1471
|
+
void err.stack;
|
|
1472
|
+
// back to the prototype's "TypeError", which is what node leaves behind. Cast because Error
|
|
1473
|
+
// declares name as always present, and this deletes the own property to uncover it again
|
|
1474
|
+
delete (/** @type {any} */ (err).name);
|
|
1475
|
+
err.code = code;
|
|
1476
|
+
return err;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1439
1479
|
/**
|
|
1440
1480
|
* Refuses a header name that is not an HTTP token, the way node's setHeader does and with its
|
|
1441
1481
|
* error, so an application catching ERR_INVALID_HTTP_TOKEN behind Express catches it here.
|
|
@@ -1446,10 +1486,7 @@ const HEADER_VALUE = /[^\t\x20-\x7e\x80-\xff]/;
|
|
|
1446
1486
|
*/
|
|
1447
1487
|
function validateHeaderName(name) {
|
|
1448
1488
|
if (typeof name !== "string" || !HEADER_TOKEN.test(name)) {
|
|
1449
|
-
|
|
1450
|
-
const err = new TypeError(`Header name must be a valid HTTP token ["${name}"]`);
|
|
1451
|
-
err.code = "ERR_INVALID_HTTP_TOKEN";
|
|
1452
|
-
throw err;
|
|
1489
|
+
throw headerError(`Header name must be a valid HTTP token ["${name}"]`, "ERR_INVALID_HTTP_TOKEN");
|
|
1453
1490
|
}
|
|
1454
1491
|
}
|
|
1455
1492
|
|
|
@@ -1471,10 +1508,7 @@ function validateHeaderValue(name, value) {
|
|
|
1471
1508
|
return;
|
|
1472
1509
|
}
|
|
1473
1510
|
if (HEADER_VALUE.test(value)) {
|
|
1474
|
-
|
|
1475
|
-
const err = new TypeError(`Invalid character in header content ["${name}"]`);
|
|
1476
|
-
err.code = "ERR_INVALID_CHAR";
|
|
1477
|
-
throw err;
|
|
1511
|
+
throw headerError(`Invalid character in header content ["${name}"]`, "ERR_INVALID_CHAR");
|
|
1478
1512
|
}
|
|
1479
1513
|
}
|
|
1480
1514
|
|