fulmine.js 5.12.2 → 5.12.3
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 -1
- package/package.json +6 -3
- package/src/application.js +2 -2
- package/src/declarative.js +16 -0
- package/src/request.js +98 -17
- package/src/router.js +61 -12
- package/src/utils.js +22 -2
package/README.md
CHANGED
|
@@ -285,7 +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
|
|
288
|
+
- **A request whose framing cannot be trusted is refused by hanging up, with no answer at all.** Node's parser refuses each of these with a `400` and Fulmine refuses the same ones: a repeated `Content-Length`; one that is not a plain count of bytes, an empty value or a count past `Number.MAX_SAFE_INTEGER` included; a `Transfer-Encoding` whose last coding is not `chunked`; and a method nobody defines, which includes a lowercase one, since methods are case sensitive. µWS accepts all of them. It frames the request on the first length, or on no body at all, and it takes any token as a method, so `{"a":1}GET /path HTTP/1.1` is a request line to it. What the client sent as a body is then read as the next request on the connection: that is request smuggling, and a proxy in front disagreeing about the framing 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 any of these.
|
|
289
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.
|
|
290
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`.
|
|
291
291
|
- For HTTPS, instead of doing this:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.12.
|
|
3
|
+
"version": "5.12.3",
|
|
4
4
|
"description": "Drop-in Express 5 replacement on uWebSockets.js. Your existing middleware keeps working.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"release:local": "node tools/release-local.js",
|
|
29
29
|
"cover:full": "nyc --silent npm run test && nyc --silent --no-clean npm run test:unit && nyc --silent --no-clean npm run test:express && nyc report",
|
|
30
30
|
"cover:check": "nyc check-coverage --statements 94.5 --branches 90 --functions 94 --lines 94.5",
|
|
31
|
-
"examples:install": "npm --prefix examples install"
|
|
31
|
+
"examples:install": "npm --prefix examples install",
|
|
32
|
+
"fuzz:wire": "node tools/wire-fuzz.js",
|
|
33
|
+
"fuzz:headers": "node tools/header-fuzz.js",
|
|
34
|
+
"fuzz:session": "node tools/session-fuzz.js"
|
|
32
35
|
},
|
|
33
36
|
"engines": {
|
|
34
37
|
"node": ">=22"
|
|
@@ -72,7 +75,7 @@
|
|
|
72
75
|
"bytes": "^3.1.2",
|
|
73
76
|
"compressible": "^2.0.18",
|
|
74
77
|
"content-disposition": "^1.1.0",
|
|
75
|
-
"cookie": "^
|
|
78
|
+
"cookie": "^0.7.2",
|
|
76
79
|
"cookie-signature": "^1.2.2",
|
|
77
80
|
"encodeurl": "^2.0.0",
|
|
78
81
|
"fast-decode-uri-component": "^1.0.1",
|
package/src/application.js
CHANGED
|
@@ -529,8 +529,8 @@ 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.
|
|
533
|
-
return this.
|
|
532
|
+
if (request._mustRefuse === true) {
|
|
533
|
+
return this._refuseRequest(response);
|
|
534
534
|
}
|
|
535
535
|
try {
|
|
536
536
|
this._routeRequestDirect(request, response);
|
package/src/declarative.js
CHANGED
|
@@ -54,6 +54,11 @@ const MAX_INSTRUCTION_LENGTH = 65535;
|
|
|
54
54
|
// so it cannot honour one, and a handler that sets one has to stay on the ordinary path.
|
|
55
55
|
const VALIDATOR_HEADERS = new Set(["etag", "last-modified"]);
|
|
56
56
|
|
|
57
|
+
// The statuses whose message carries no content. 205 is here for node's reason rather than
|
|
58
|
+
// express's: express strips the body for 204 and 304, and node answers a 205 with a lone
|
|
59
|
+
// Content-Length of zero, so all three come out of the ordinary path with no body at all.
|
|
60
|
+
const BODILESS_STATUSES = new Set([204, 205, 304]);
|
|
61
|
+
|
|
57
62
|
const bodyMethods = new Set(["send", "json", "end"]);
|
|
58
63
|
// and the four that finish the response, after which nothing a handler does is observable
|
|
59
64
|
const terminalMethods = new Set(["send", "json", "end", "sendStatus"]);
|
|
@@ -710,6 +715,17 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
710
715
|
return false;
|
|
711
716
|
}
|
|
712
717
|
|
|
718
|
+
// A status that carries no content, which is a rule about the message and not about the
|
|
719
|
+
// application: express drops the body and the framing headers for 204 and 304, and node
|
|
720
|
+
// writes a lone Content-Length: 0 for 205. Compiled, the body went out anyway, so
|
|
721
|
+
// res.sendStatus(204) answered "No Content" with a Content-Length of ten. A client frames
|
|
722
|
+
// a 204 as bodiless whatever the headers say, so those ten bytes were read as the start of
|
|
723
|
+
// the next answer on the connection, which is a desync on any keep-alive client. The
|
|
724
|
+
// ordinary path already writes all three the way express does: this hands them back to it.
|
|
725
|
+
if (BODILESS_STATUSES.has(statusCode) || statusCode < 200) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
|
|
713
729
|
let decRes = new uWSAny.DeclarativeResponse();
|
|
714
730
|
|
|
715
731
|
if (statusCode !== 200) {
|
package/src/request.js
CHANGED
|
@@ -152,6 +152,52 @@ 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
|
+
// The methods node's parser accepts, which is the set a request can arrive with behind Express and
|
|
156
|
+
// the set a route can be registered for here. µWS accepts any token, so without this a line like
|
|
157
|
+
// `{"a":1}GET /path HTTP/1.1` is a request to it, with `{"A":1}GET` as the method. See _mustRefuse.
|
|
158
|
+
const KNOWN_METHODS = new Set(require("http").METHODS);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether a transfer-encoding leaves the body's length knowable, which is RFC 9112's rule that
|
|
162
|
+
* `chunked` comes last. `gzip, chunked` is fine and `chunked, gzip` is not: with a coding applied
|
|
163
|
+
* after the framing one, nothing can say where the body ends, and node answers 400 rather than
|
|
164
|
+
* guess. µWS guesses, and what it guesses wrong becomes the next request on the connection.
|
|
165
|
+
*
|
|
166
|
+
* Read per header rather than over the joined value, so a request splitting the list across two
|
|
167
|
+
* transfer-encoding headers is refused even when the codings would be legal joined up. That is
|
|
168
|
+
* stricter than node by a hair, on a shape nothing sends, and stricter is the safe direction here.
|
|
169
|
+
*
|
|
170
|
+
* @param {string} value one transfer-encoding header, as uWS hands it over
|
|
171
|
+
* @returns {boolean}
|
|
172
|
+
*/
|
|
173
|
+
function endsWithChunked(value) {
|
|
174
|
+
const last = value.slice(value.lastIndexOf(",") + 1).trim();
|
|
175
|
+
// a coding may carry parameters, which are not part of its name
|
|
176
|
+
const semicolon = last.indexOf(";");
|
|
177
|
+
return (semicolon === -1 ? last : last.slice(0, semicolon)).trim().toLowerCase() === "chunked";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The path of the url a request carries right now, without the query.
|
|
182
|
+
*
|
|
183
|
+
* Express reads it off req.url on every access, so a middleware that assigns req.url is seen by
|
|
184
|
+
* whatever runs next, the callback after it in the same route included: the router takes a rewrite
|
|
185
|
+
* over at its next hop, and until then the field is a hop behind. `req.url = req.url.replace(/^\/+/,
|
|
186
|
+
* "/")` reported "//" for the rest of its own route where express reports "/". The field answers
|
|
187
|
+
* while the two agree, which is every read of a request nobody rewrote.
|
|
188
|
+
*
|
|
189
|
+
* @param {any} req
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
function currentPath(req) {
|
|
193
|
+
const url = req.url;
|
|
194
|
+
if (url === req._lastUrl) {
|
|
195
|
+
return req._path;
|
|
196
|
+
}
|
|
197
|
+
const query = url.indexOf("?");
|
|
198
|
+
return query === -1 ? url : url.slice(0, query);
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
/**
|
|
156
202
|
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
157
203
|
*
|
|
@@ -173,6 +219,12 @@ function isByteCount(value) {
|
|
|
173
219
|
return false;
|
|
174
220
|
}
|
|
175
221
|
}
|
|
222
|
+
// A count nothing can represent is not a count. Node refuses one that overflows, and µWS framed
|
|
223
|
+
// the request as if it had said something else, which put the bytes after it in a request of
|
|
224
|
+
// their own. The length test first, so an ordinary value never parses.
|
|
225
|
+
if (value.length > 15 && Number(value) > Number.MAX_SAFE_INTEGER) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
176
228
|
return true;
|
|
177
229
|
}
|
|
178
230
|
|
|
@@ -363,11 +415,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
363
415
|
if (headerKey.length === 14) {
|
|
364
416
|
// a second content-length whatever it says, and one that is not a count of bytes:
|
|
365
417
|
// both make uWS frame the request differently from what is on the wire, see
|
|
366
|
-
//
|
|
418
|
+
// _mustRefuse and isByteCount
|
|
367
419
|
if (r._sawContentLength || !isByteCount(value)) {
|
|
368
|
-
r.
|
|
420
|
+
r._mustRefuse = true;
|
|
369
421
|
}
|
|
370
422
|
r._sawContentLength = true;
|
|
423
|
+
} else if (!endsWithChunked(value)) {
|
|
424
|
+
// chunked has to be the last coding: anything after it and the length of the body
|
|
425
|
+
// is not knowable, which node answers 400 to and µWS served. See endsWithChunked
|
|
426
|
+
r._mustRefuse = true;
|
|
371
427
|
}
|
|
372
428
|
// saying anything about framing at all, "0" included. A parser that can see a
|
|
373
429
|
// content-length answers about the body it describes, even an empty one: a zero length
|
|
@@ -480,9 +536,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
480
536
|
_sawContentLength;
|
|
481
537
|
|
|
482
538
|
/**
|
|
483
|
-
* Whether
|
|
484
|
-
*
|
|
485
|
-
*
|
|
539
|
+
* Whether this request must not be routed at all. Node's parser refuses each of these outright
|
|
540
|
+
* and answers 400; every one of them is a way for bytes the client did not send as a request to
|
|
541
|
+
* be served as one, which is request smuggling.
|
|
486
542
|
*
|
|
487
543
|
* a repeated content-length uWS frames on the first and drops the rest, so a proxy in
|
|
488
544
|
* front reading the last one instead forwards bytes uWS then
|
|
@@ -491,13 +547,17 @@ module.exports = class Request extends LazyReadable {
|
|
|
491
547
|
* included, and frames the request as carrying no body at all,
|
|
492
548
|
* which turns the body the client sent into that same second
|
|
493
549
|
* request. See isByteCount
|
|
550
|
+
* a method nobody defines uWS takes any token as the method, so anything at all
|
|
551
|
+
* followed by a space and a path is a request line to it. A
|
|
552
|
+
* request with no content-length and no transfer-encoding has
|
|
553
|
+
* no body, so the bytes after it are the next request: node
|
|
554
|
+
* reads them and answers 400, uWS served them. See KNOWN_METHODS
|
|
494
555
|
*
|
|
495
|
-
*
|
|
496
|
-
* for the same reason as rawIp.
|
|
556
|
+
* Declared for the same reason as rawIp.
|
|
497
557
|
*
|
|
498
558
|
* @type {boolean|undefined}
|
|
499
559
|
*/
|
|
500
|
-
|
|
560
|
+
_mustRefuse;
|
|
501
561
|
|
|
502
562
|
/**
|
|
503
563
|
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
@@ -567,7 +627,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
567
627
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
568
628
|
// getHeader only ever returns the first of a repeated header, so a duplicate cannot be
|
|
569
629
|
// seen from here, and a duplicate has to be refused rather than routed: see
|
|
570
|
-
//
|
|
630
|
+
// _mustRefuse. Anything that says a word about framing takes the full copy instead.
|
|
571
631
|
//
|
|
572
632
|
// One shape stays invisible here, a content-length present with an empty value: uWS
|
|
573
633
|
// answers "" for that and for a header that was never sent, and nothing in its API
|
|
@@ -632,7 +692,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
632
692
|
}
|
|
633
693
|
if (preset) {
|
|
634
694
|
// the registration's constants: two native crossings and their strings not asked for
|
|
635
|
-
this.
|
|
695
|
+
this._path = preset.path;
|
|
636
696
|
this.originalUrl = preset.path + this.urlQuery;
|
|
637
697
|
this.url = this.originalUrl;
|
|
638
698
|
this._lastUrl = this.originalUrl;
|
|
@@ -646,17 +706,27 @@ module.exports = class Request extends LazyReadable {
|
|
|
646
706
|
// getUrl() is the path already, so the query is joined on and then not split off
|
|
647
707
|
// again. Building originalUrl and picking the path back out of it with indexOf and
|
|
648
708
|
// substring was a search and a second string for something uWS had just handed over.
|
|
649
|
-
this.
|
|
650
|
-
this.originalUrl = this.
|
|
709
|
+
this._path = req.getUrl();
|
|
710
|
+
this.originalUrl = this._path + this.urlQuery;
|
|
651
711
|
this.url = this.originalUrl;
|
|
652
712
|
// what the router last wrote to req.url. A middleware assigning something else is a
|
|
653
713
|
// rewrite, which express honours, and dispatch compares against this to notice it
|
|
654
714
|
this._lastUrl = this.originalUrl;
|
|
655
715
|
// charCodeAt rather than indexing: s[i] builds a one character string to throw away
|
|
656
|
-
this.endsWithSlash = this.
|
|
657
|
-
this._opPath = this.
|
|
658
|
-
this._originalPath = this.
|
|
659
|
-
|
|
716
|
+
this.endsWithSlash = this._path.charCodeAt(this._path.length - 1) === 0x2f;
|
|
717
|
+
this._opPath = this._path;
|
|
718
|
+
this._originalPath = this._path;
|
|
719
|
+
const rawMethod = req.getCaseSensitiveMethod();
|
|
720
|
+
this.method = rawMethod.toUpperCase();
|
|
721
|
+
// node's parser knows a fixed set and refuses everything else; µWS takes the token as
|
|
722
|
+
// it finds it, so a request line is anything with a space in it. Compared before the
|
|
723
|
+
// uppercasing on purpose: a method is case sensitive, node refuses "post", and µWS
|
|
724
|
+
// folds it to POST and serves it. Only asked of a method the framework cannot route
|
|
725
|
+
// anyway, since a route can only be registered for one of these, see the loop that
|
|
726
|
+
// builds the verb methods at the end of router.js
|
|
727
|
+
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
728
|
+
this._mustRefuse = true;
|
|
729
|
+
}
|
|
660
730
|
this._isOptions = this.method === "OPTIONS";
|
|
661
731
|
this._isHead = this.method === "HEAD";
|
|
662
732
|
}
|
|
@@ -1004,6 +1074,17 @@ module.exports = class Request extends LazyReadable {
|
|
|
1004
1074
|
return index !== -1 ? header.slice(0, index).trim() : header.trim();
|
|
1005
1075
|
}
|
|
1006
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* The path of the current url, without the query and relative to the mount the request is in.
|
|
1079
|
+
* A getter rather than a field, because express recomputes it from req.url on every read, see
|
|
1080
|
+
* currentPath.
|
|
1081
|
+
*
|
|
1082
|
+
* @returns {string}
|
|
1083
|
+
*/
|
|
1084
|
+
get path() {
|
|
1085
|
+
return currentPath(this);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1007
1088
|
/**
|
|
1008
1089
|
* Takes over what a middleware assigned to req.url: the remaining routing matches the new
|
|
1009
1090
|
* path, and req.query reflects the new query string. The assigned url is relative to the
|
|
@@ -1025,7 +1106,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1025
1106
|
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
1026
1107
|
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
1027
1108
|
this._originalPath = prefix + newPath;
|
|
1028
|
-
this.
|
|
1109
|
+
this._path = newPath;
|
|
1029
1110
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
1030
1111
|
this._opPath = newPath;
|
|
1031
1112
|
this._opPathLower = null;
|
package/src/router.js
CHANGED
|
@@ -354,6 +354,13 @@ class Walk {
|
|
|
354
354
|
runRoute(continueRoute) {
|
|
355
355
|
const req = this.req;
|
|
356
356
|
const route = this.route;
|
|
357
|
+
// A compiled chain walks into a mount rather than entering it, so the rule above needs
|
|
358
|
+
// saying here as well: everything after this marker is inside the mount, and a mount is
|
|
359
|
+
// stepped over while an error is in flight. Leaving the chain is what running out of it
|
|
360
|
+
// already means, and ordinary routing takes over after the mount.
|
|
361
|
+
if (route.keepMount === true && req._error) {
|
|
362
|
+
return this.dispatch(this.routes.length);
|
|
363
|
+
}
|
|
357
364
|
if (route.use) {
|
|
358
365
|
if (route.mountApp) {
|
|
359
366
|
// optimized chain: normal dispatch swaps req.app when it enters a mounted
|
|
@@ -484,7 +491,12 @@ class Walk {
|
|
|
484
491
|
if (!this.skipCheck && this.skipUntil && this.skipUntil.routeKey >= route.routeKey) {
|
|
485
492
|
return this.step(undefined);
|
|
486
493
|
}
|
|
487
|
-
|
|
494
|
+
// A mounted router or application is stepped over while an error is in flight. Its handle
|
|
495
|
+
// takes three arguments, so express's Layer#handleError hands the error straight on without
|
|
496
|
+
// entering it: what a mount catches is what it raised itself. Entering it ran the error
|
|
497
|
+
// handlers written inside the mount, and left req.app pointing at a mounted application,
|
|
498
|
+
// whose settings then answered. A 500 carried an ETag under app.set("etag", false).
|
|
499
|
+
if (kind === CALLBACK_ROUTER && !req._error) {
|
|
488
500
|
if (callback._isApplication) {
|
|
489
501
|
rememberApp(this, route, req);
|
|
490
502
|
useApp(req, callback);
|
|
@@ -683,10 +695,18 @@ function setMountedPath(req) {
|
|
|
683
695
|
req._opPath = req._consumed === 0 ? req._originalPath : req._originalPath.slice(req._consumed);
|
|
684
696
|
req._opPathLower = null;
|
|
685
697
|
req.url = req._opPath === "" ? "/" + req.urlQuery : req._opPath + req.urlQuery;
|
|
686
|
-
req.
|
|
698
|
+
req._path = req._opPath === "" ? "/" : req._opPath;
|
|
687
699
|
req._lastUrl = req.url;
|
|
688
700
|
}
|
|
689
701
|
|
|
702
|
+
// req.path as the request class declares it, taken off the prototype rather than written out a
|
|
703
|
+
// second time. A request the router adopts is a plain object and gets it defined on itself, see
|
|
704
|
+
// adoptPlainRequest. Enumerable, as express's own is.
|
|
705
|
+
const PATH_PROPERTY = {
|
|
706
|
+
.../** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Request.prototype, "path")),
|
|
707
|
+
enumerable: true
|
|
708
|
+
};
|
|
709
|
+
|
|
690
710
|
const NO_PARAM_NAMES = [];
|
|
691
711
|
|
|
692
712
|
/**
|
|
@@ -830,7 +850,10 @@ function adoptPlainRequest(req, router) {
|
|
|
830
850
|
const path = queryIndex === -1 ? raw : raw.slice(0, queryIndex);
|
|
831
851
|
req.urlQuery = queryIndex === -1 ? "" : raw.slice(queryIndex);
|
|
832
852
|
req._rawQuery = req.urlQuery.slice(1);
|
|
833
|
-
req.
|
|
853
|
+
req._path = path;
|
|
854
|
+
// an adopted request is a plain object, so it carries no prototype of ours and reads its path
|
|
855
|
+
// off a property of its own. The class's getter itself, so there is one of it
|
|
856
|
+
Object.defineProperty(req, "path", PATH_PROPERTY);
|
|
834
857
|
req.originalUrl = req.originalUrl ?? arrived;
|
|
835
858
|
req._originalPath = path;
|
|
836
859
|
req.endsWithSlash = path.charCodeAt(path.length - 1) === 0x2f;
|
|
@@ -1848,6 +1871,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1848
1871
|
}
|
|
1849
1872
|
}
|
|
1850
1873
|
|
|
1874
|
+
// The same rule as the one just above, which a route of another method reaches by
|
|
1875
|
+
// another road. A mount's chain is inherited by every path under it, and a route that
|
|
1876
|
+
// is not itself a mount answers the mount point rather than the subtree: in the chain
|
|
1877
|
+
// it ran for the whole of it, so router.all("/:p1") answered the /posts/a-b that
|
|
1878
|
+
// belongs to the router mounted at /posts. guardsInside is written for this: only
|
|
1879
|
+
// layers with more segments than the mount path are asked about a leaf.
|
|
1880
|
+
if (route.use && !r.use && typeof route.path === "string" && couldAnswer(r, route.path)) {
|
|
1881
|
+
return false;
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1851
1884
|
// a RegExp mount runs only where its match starts the path and breaks on a separator,
|
|
1852
1885
|
// which is decidable here against a literal path and not against one with a parameter
|
|
1853
1886
|
if (r.regexMount) {
|
|
@@ -1874,13 +1907,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1874
1907
|
|
|
1875
1908
|
// check if the paths match. A route with parameters is excluded from the text test:
|
|
1876
1909
|
// its literal ":name" text would let an earlier regex in on requests it never matches.
|
|
1877
|
-
|
|
1878
|
-
// answers "/x/" as well as "/x", and an earlier pattern that matches only the first is
|
|
1879
|
-
// still an earlier pattern that answers a request this registration would take.
|
|
1910
|
+
const regexCanMatch = r.pattern instanceof RegExp && (!withParams || r.use);
|
|
1880
1911
|
if (
|
|
1881
|
-
(r.pattern
|
|
1882
|
-
(!withParams || r.use) &&
|
|
1883
|
-
(r.pattern.test(route.path) || (!strictHere && r.pattern.test(route.path + "/")))) ||
|
|
1912
|
+
(regexCanMatch && r.pattern.test(route.path)) ||
|
|
1884
1913
|
(typeof r.pattern === "string" &&
|
|
1885
1914
|
(r.pattern === route.path ||
|
|
1886
1915
|
(!caseSensitive && r.pattern.toLowerCase() === routePathFolded) ||
|
|
@@ -1892,6 +1921,15 @@ module.exports = class Router extends EventEmitter {
|
|
|
1892
1921
|
optimizedPath.push(r);
|
|
1893
1922
|
continue;
|
|
1894
1923
|
}
|
|
1924
|
+
// Without strict routing this registration answers "/x/" as well as "/x". An earlier
|
|
1925
|
+
// pattern matching only the second answers part of what the registration takes and not
|
|
1926
|
+
// the rest, which the chain has no way to say: it runs what is in it without matching
|
|
1927
|
+
// again. Both spellings used to put the route in whole, so app.all("/:p0/{:o1}/{:o2}")
|
|
1928
|
+
// answered a GET /list/Mixed that belonged to the route written after it. An ordinary
|
|
1929
|
+
// pattern answers both spellings, so only an optional group or a wildcard reaches here.
|
|
1930
|
+
if (regexCanMatch && !strictHere && r.pattern.test(route.path + "/")) {
|
|
1931
|
+
return false;
|
|
1932
|
+
}
|
|
1895
1933
|
if (!withParams) {
|
|
1896
1934
|
continue;
|
|
1897
1935
|
}
|
|
@@ -1951,6 +1989,13 @@ module.exports = class Router extends EventEmitter {
|
|
|
1951
1989
|
if (!this.uwsApp) {
|
|
1952
1990
|
return;
|
|
1953
1991
|
}
|
|
1992
|
+
// Everything below is what makes this framework fast, and every one of its decisions is a
|
|
1993
|
+
// claim that µWS answering by itself is the same answer the chain would have given. Turned
|
|
1994
|
+
// off, the claim is not made and the chain answers everything. Serving one application both
|
|
1995
|
+
// ways and comparing the answers is what tests those claims: `npm run fuzz -- --self`.
|
|
1996
|
+
if (this.get("native routes") === false) {
|
|
1997
|
+
return;
|
|
1998
|
+
}
|
|
1954
1999
|
|
|
1955
2000
|
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
1956
2001
|
// carries what was written before them and answers only part of what is under them
|
|
@@ -2135,7 +2180,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
2135
2180
|
*
|
|
2136
2181
|
* @param {any} response
|
|
2137
2182
|
*/
|
|
2138
|
-
|
|
2183
|
+
_refuseRequest(response) {
|
|
2139
2184
|
response.finished = true;
|
|
2140
2185
|
response._res.close();
|
|
2141
2186
|
response.emit("close");
|
|
@@ -2247,8 +2292,8 @@ module.exports = class Router extends EventEmitter {
|
|
|
2247
2292
|
}
|
|
2248
2293
|
const request = this.handleRequest(res, req, preset, skipHolder);
|
|
2249
2294
|
const response = request.res;
|
|
2250
|
-
if (request.
|
|
2251
|
-
return this.
|
|
2295
|
+
if (request._mustRefuse === true) {
|
|
2296
|
+
return this._refuseRequest(response);
|
|
2252
2297
|
}
|
|
2253
2298
|
if (optimizedParams) {
|
|
2254
2299
|
request.optimizedParams = new NullObject();
|
|
@@ -2347,6 +2392,10 @@ module.exports = class Router extends EventEmitter {
|
|
|
2347
2392
|
route.callbacks.length === 1 && // must not have multiple callbacks
|
|
2348
2393
|
typeof route.callbacks[0] === "function" && // must be a function
|
|
2349
2394
|
route.paramCallbacks.size === 0 && // a param callback has to run, and this answers without running anything
|
|
2395
|
+
// a captured value is decoded when the route runs, and one that cannot be decoded is a
|
|
2396
|
+
// 400 in express and on the ordinary path here. Nothing runs to raise it on a
|
|
2397
|
+
// declarative response, so GET /a-b%5Ec@d%e came back 200 from app.get("/:p12")
|
|
2398
|
+
route.optimizedParams === undefined &&
|
|
2350
2399
|
// a declarative response is answered by µWS itself, so no javascript runs and the case
|
|
2351
2400
|
// guard could not: a route that needs one has to stay an ordinary handler
|
|
2352
2401
|
caseGuards === null &&
|
package/src/utils.js
CHANGED
|
@@ -409,8 +409,21 @@ function patternToRegex(pattern, isPrefix = false, caseSensitive = true, strict
|
|
|
409
409
|
// splits /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext
|
|
410
410
|
// cannot contain a dot. After static text there is nothing to give ground, so the
|
|
411
411
|
// parameter takes everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
|
|
412
|
-
|
|
413
|
-
|
|
412
|
+
// The whole of it, not its first character: /:foo{abc:bar} against /123abcabc splits as
|
|
413
|
+
// foo=123 and bar=abc on express, and reading the separator as "a" left bar unable to
|
|
414
|
+
// match its own text, so the group never matched and foo took the segment whole. More
|
|
415
|
+
// than one character cannot go in a class, so it is written as a lookahead, and either
|
|
416
|
+
// way the parameter is still allowed to be exactly the separator, as path-to-regexp
|
|
417
|
+
// writes it.
|
|
418
|
+
const colon = groupContent.indexOf(":");
|
|
419
|
+
const separator = lastTokenWasParam && colon > 0 ? groupContent.slice(0, colon) : "";
|
|
420
|
+
const escapedSeparator = separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
421
|
+
const groupParamClass =
|
|
422
|
+
separator === ""
|
|
423
|
+
? "[^/]+"
|
|
424
|
+
: separator.length === 1
|
|
425
|
+
? `[^/${escapedSeparator}]+|${escapedSeparator}`
|
|
426
|
+
: `(?:(?!${escapedSeparator})[^/])+|${escapedSeparator}`;
|
|
414
427
|
|
|
415
428
|
let groupRegex = "";
|
|
416
429
|
let gi = 0;
|
|
@@ -961,6 +974,13 @@ const defaultSettings = {
|
|
|
961
974
|
// The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
|
|
962
975
|
// it routes whose earlier siblings it can prove agree under either case rule.
|
|
963
976
|
"declarative responses": true,
|
|
977
|
+
// on. Off hands every request to the ordinary chain instead of letting µWS match what it can,
|
|
978
|
+
// which is slower and answers the same. Not a tuning knob: it exists so one application can be
|
|
979
|
+
// served both ways and the two sets of answers compared, which tests the optimizer against the
|
|
980
|
+
// rest of the framework without a second framework to compare with. See
|
|
981
|
+
// `npm run fuzz -- --self`. A compiled response needs a native registration to hang on, so this
|
|
982
|
+
// takes "declarative responses" with it.
|
|
983
|
+
"native routes": true,
|
|
964
984
|
// off: with a window set, the size and mtime of a file served by sendFile are remembered for
|
|
965
985
|
// it, which is one syscall less per request and a file that can be served as it was a moment
|
|
966
986
|
// ago. "stat cache ms" is the window in milliseconds, compiled from it by set()
|