fulmine.js 5.3.0 → 5.4.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 +87 -3
- package/package.json +7 -2
- package/src/declarative.js +9 -0
- package/src/node-shim.js +11 -0
- package/src/request.js +84 -26
- package/src/response.js +126 -11
- package/src/router.js +137 -20
- package/src/utils.js +5 -1
package/README.md
CHANGED
|
@@ -53,6 +53,7 @@ shared machine, so the number would describe the machine rather than the framewo
|
|
|
53
53
|
- [WebSockets](#websockets)
|
|
54
54
|
- [socket.io](#socketio)
|
|
55
55
|
- [HTTP/3](#http3)
|
|
56
|
+
- [Behind a proxy](#behind-a-proxy)
|
|
56
57
|
- [Versioning](#versioning)
|
|
57
58
|
- [Compatibility](#compatibility)
|
|
58
59
|
- [express](#express)
|
|
@@ -95,7 +96,7 @@ to run it yourself.
|
|
|
95
96
|
Numbers produced by a project about itself deserve suspicion, so Fulmine also stands in public arenas, run by their own rigs under their own rules:
|
|
96
97
|
|
|
97
98
|
- **[HttpArena](https://www.http-arena.com/#sort=rps:-1&q=Js)** (the link lands filtered on the JavaScript entries): first among the JavaScript entries, across fifteen subscribed profiles. The saved runs measure 24.3 million WebSocket echoes per second pipelined and 3.77 million one-at-a-time (past Bun's own dedicated WebSocket entry), 7.3 million pipelined HTTP requests per second, 1.12 million on the json profile with 1.04 million of that surviving TLS, 457 thousand on compressed json, and 359 thousand on the Postgres CRUD profile, within ten percent of the leading Rust and C# entries there.
|
|
98
|
-
- **[web-frameworks](https://
|
|
99
|
+
- **[web-frameworks](https://web-frameworks-benchmark.netlify.app/result?l=javascript)**: entry merged, numbers arrive with their next published round.
|
|
99
100
|
|
|
100
101
|
More to come as their maintainers take the entries in.
|
|
101
102
|
|
|
@@ -246,6 +247,56 @@ app.listen(3000, () => {
|
|
|
246
247
|
|
|
247
248
|
## Performance tips
|
|
248
249
|
|
|
250
|
+
Where the speed comes from, before the rules that govern it. Express finds a route by walking its
|
|
251
|
+
stack and testing each layer against the path. Fulmine hands every route it can to µWS's own router,
|
|
252
|
+
which matches in C++, and works out at `listen()` which layers stand in front of each one, so
|
|
253
|
+
arriving at a handler costs no matching at all:
|
|
254
|
+
|
|
255
|
+
```text
|
|
256
|
+
Express Fulmine
|
|
257
|
+
GET /users/42 GET /users/42
|
|
258
|
+
| |
|
|
259
|
+
v v
|
|
260
|
+
+--------------+ +------------------+
|
|
261
|
+
| layer 1 | path? no | µWS router | one match, in C++,
|
|
262
|
+
| layer 2 | path? no | /users/:id | against every path
|
|
263
|
+
| ... | +--------+---------+ registered
|
|
264
|
+
| layer 214 | path? yes -+ |
|
|
265
|
+
+--------------+ | v
|
|
266
|
+
a test per layer, | +------------------+
|
|
267
|
+
every request | | the chain, known | the layers in front,
|
|
268
|
+
| | since listen() | in order, no matching
|
|
269
|
+
v +--------+---------+
|
|
270
|
+
handler |
|
|
271
|
+
v
|
|
272
|
+
handler
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
That is the whole difference on a large route table: the scan grows with the table and the match
|
|
276
|
+
does not, which is why a thousand routes measure 10x and a handful measure 3x.
|
|
277
|
+
|
|
278
|
+
Two more things happen on the way in, and `npx fulmine profile` will tell you which of them your
|
|
279
|
+
routes get:
|
|
280
|
+
|
|
281
|
+
```text
|
|
282
|
+
a request arriving at a compiled route
|
|
283
|
+
|
|
284
|
+
µWS match ──► the chain ──────────────────────────► handler ──► response
|
|
285
|
+
| |
|
|
286
|
+
| a body parser is stepped over | the Readable is not
|
|
287
|
+
| when the request declared no | built unless something
|
|
288
|
+
| body and the verb reads none | asks the body for one
|
|
289
|
+
| |
|
|
290
|
+
| the headers are not copied out | the two internal
|
|
291
|
+
| of µWS when the analysis proved | listeners are written
|
|
292
|
+
| nothing in the chain reads one | into the event map
|
|
293
|
+
v v
|
|
294
|
+
work that does not happen work that is not prepared
|
|
295
|
+
|
|
296
|
+
and when the handler is simple enough to be read at registration time, none of the
|
|
297
|
+
above happens either: µWS answers from a response written once, at startup
|
|
298
|
+
```
|
|
299
|
+
|
|
249
300
|
1. Fulmine tries to optimize routing as much as possible, but it's only possible if:
|
|
250
301
|
|
|
251
302
|
- the path is a plain string, or its parameters are whole segments: `/users/:id` and `/a/:b/c/:d` qualify, `/flights/:from-:to` does not, and neither does a `*splat` or a `{}` group. Routing is case-insensitive by default, as in Express; a request in the registered case is still served natively, any other case takes the ordinary path, and a route whose overlap with an earlier one leans on a cased literal goes the ordinary way for every request.
|
|
@@ -378,6 +429,32 @@ const app = express({
|
|
|
378
429
|
});
|
|
379
430
|
```
|
|
380
431
|
|
|
432
|
+
## Behind a proxy
|
|
433
|
+
|
|
434
|
+
`trust proxy` works as it does in Express: set it and `req.ip`, `req.ips`, `req.protocol` and
|
|
435
|
+
`req.hostname` are read from `X-Forwarded-*` when the connection comes from a peer you trust.
|
|
436
|
+
|
|
437
|
+
Fulmine adds the other way of being told, the one that does not use headers at all. HAProxy, AWS
|
|
438
|
+
NLB, nginx with `proxy_protocol` and Envoy can prepend a **PROXY protocol** preamble to the
|
|
439
|
+
connection, and µWebSockets.js parses it. Off by default, and one line turns it on:
|
|
440
|
+
|
|
441
|
+
```js
|
|
442
|
+
app.set("trust proxy protocol", true);
|
|
443
|
+
// req.ip, req.socket.remoteAddress and everything reading them are now the address the proxy
|
|
444
|
+
// declared, and fall back to the socket's own on a connection that sent no preamble
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
> [!WARNING]
|
|
448
|
+
> **Only turn this on when nothing but the proxy can reach the server.** µWS reads the preamble
|
|
449
|
+
> from whoever sends it. There is no way to say which peers may use it, so on a port open to the
|
|
450
|
+
> internet the first sixteen bytes of any connection are enough for a client to become `10.0.0.1`
|
|
451
|
+
> for your rate limiter, your allow list and your audit log. Bind to the private interface, or
|
|
452
|
+
> keep this off.
|
|
453
|
+
|
|
454
|
+
`trust proxy` and this can both be on. The preamble decides what the connection's address is, and
|
|
455
|
+
`trust proxy` then peels `X-Forwarded-For` off that, so a proxy that sends both is read the way it
|
|
456
|
+
meant.
|
|
457
|
+
|
|
381
458
|
## Versioning
|
|
382
459
|
|
|
383
460
|
**The major number tracks Express, not semver.** Fulmine 5.x follows Express 5. If Express 6
|
|
@@ -461,9 +538,11 @@ Two of these keep a compiled form alongside the value, which you can also set di
|
|
|
461
538
|
- `etag fn`, the function that produces an ETag. Setting `etag` compiles one; setting this replaces it.
|
|
462
539
|
- `query parser fn`, likewise for `query parser`.
|
|
463
540
|
|
|
464
|
-
Fulmine adds
|
|
541
|
+
Fulmine adds three of its own:
|
|
465
542
|
|
|
466
543
|
- `declarative responses`, on by default. Lets a simple enough handler be compiled into a native uWS response, described under [Performance tips](#performance-tips).
|
|
544
|
+
- `file cache`, on by default. Small files served by `res.sendFile` come from a bounded in-process cache, checked against the file's `stat` on every request, so an edited file is never served stale.
|
|
545
|
+
- `trust proxy protocol`, off by default. Takes `req.ip` from a PROXY protocol preamble, described under [Behind a proxy](#behind-a-proxy). Read the warning there before turning it on.
|
|
467
546
|
|
|
468
547
|
### Request
|
|
469
548
|
|
|
@@ -582,7 +661,8 @@ Almost all middlewares that are compatible with Express are compatible with Fulm
|
|
|
582
661
|
- ✅ [express-rate-limit](https://npmjs.com/package/express-rate-limit)
|
|
583
662
|
- ✅ [express-subdomain](https://npmjs.com/package/express-subdomain)
|
|
584
663
|
- ✅ [vhost](https://npmjs.com/package/vhost)
|
|
585
|
-
- ✅ [
|
|
664
|
+
- ✅ [http-proxy-middleware](https://www.npmjs.com/package/http-proxy-middleware)
|
|
665
|
+
- ✅ [express-http-proxy](https://www.npmjs.com/package/express-http-proxy)
|
|
586
666
|
- ✅ [express-mongo-sanitize](https://www.npmjs.com/package/express-mongo-sanitize)
|
|
587
667
|
- ✅ [helmet](https://www.npmjs.com/package/helmet)
|
|
588
668
|
- ✅ [passport](https://www.npmjs.com/package/passport)
|
|
@@ -592,6 +672,10 @@ Almost all middlewares that are compatible with Express are compatible with Fulm
|
|
|
592
672
|
- ✅ [better-sse](https://www.npmjs.com/package/better-sse)
|
|
593
673
|
- ✅ [supertest](https://www.npmjs.com/package/supertest)
|
|
594
674
|
|
|
675
|
+
[tsoa](https://github.com/lukeautry/tsoa) works too, but it is not in the suite above: it resolves
|
|
676
|
+
`express` itself, so testing it here needs a dependency override rather than the one-line swap
|
|
677
|
+
everything else takes.
|
|
678
|
+
|
|
595
679
|
## Tested view engines
|
|
596
680
|
|
|
597
681
|
Any Express view engine should work. Here's list of engines we include in our test suite:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fulmine.js",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
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": {
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"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",
|
|
31
31
|
"cover:check": "nyc check-coverage --statements 93 --branches 88 --functions 92 --lines 93",
|
|
32
32
|
"demo:start": "npm --prefix demo install && npm --prefix demo start",
|
|
33
|
-
"demo:deploy": "
|
|
33
|
+
"demo:deploy": "cd demo && fly deploy",
|
|
34
34
|
"demo:logs": "fly logs --app fulmine-demo"
|
|
35
35
|
},
|
|
36
36
|
"engines": {
|
|
@@ -128,6 +128,7 @@
|
|
|
128
128
|
"exit-hook": "^2.2.1",
|
|
129
129
|
"express": "^5",
|
|
130
130
|
"express-art-template": "^1.0.1",
|
|
131
|
+
"express-basic-auth": "^1.2.1",
|
|
131
132
|
"express-dot-engine": "^1.0.8",
|
|
132
133
|
"express-fast-json-stringify": "^1.3.0",
|
|
133
134
|
"express-fileupload": "^1.5.2",
|
|
@@ -137,6 +138,7 @@
|
|
|
137
138
|
"express-rate-limit": "^8.5.2",
|
|
138
139
|
"express-session": "^1.19.0",
|
|
139
140
|
"express-subdomain": "^1.0.6",
|
|
141
|
+
"express-validator": "^7.3.2",
|
|
140
142
|
"fast-querystring": "^1.1.2",
|
|
141
143
|
"globals": "^17.8.0",
|
|
142
144
|
"graphql-http": "^1.22.4",
|
|
@@ -149,6 +151,8 @@
|
|
|
149
151
|
"multer": "^2.1.1",
|
|
150
152
|
"mustache-express": "^1.3.2",
|
|
151
153
|
"nyc": "^17.1.0",
|
|
154
|
+
"on-finished": "^2.4.1",
|
|
155
|
+
"on-headers": "^1.1.0",
|
|
152
156
|
"pako": "^2.1.0",
|
|
153
157
|
"passport": "^0.7.0",
|
|
154
158
|
"passport-local": "^1.0.0",
|
|
@@ -157,6 +161,7 @@
|
|
|
157
161
|
"pug": "^3.0.4",
|
|
158
162
|
"release-it": "^21.0.1",
|
|
159
163
|
"response-time": "^2.3.4",
|
|
164
|
+
"serve-favicon": "^2.5.1",
|
|
160
165
|
"serve-index": "^1.9.2",
|
|
161
166
|
"serve-static": "^2.2.1",
|
|
162
167
|
"socket.io": "^4.8.3",
|
package/src/declarative.js
CHANGED
|
@@ -166,6 +166,15 @@ module.exports = function compileDeclarative(cb, app) {
|
|
|
166
166
|
// out as `return false`, which is the fallback to ordinary routing. That is the design, and
|
|
167
167
|
// it is why the tree is walked loosely: acorn types every shape JavaScript can take, and
|
|
168
168
|
// enumerating them here would be a second, worse copy of that catch.
|
|
169
|
+
//
|
|
170
|
+
// The list below looks like the thing to widen, and it is not. Counted over the 1113
|
|
171
|
+
// handlers in this repository's tests, demo and benchmark scenarios: 42.6% call something
|
|
172
|
+
// that is not res, which no syntax admitted here can reach, and admitting `const` would
|
|
173
|
+
// unlock 7 handlers, 0.6%, one conditional a further 0.1%. On the demo and the benchmark
|
|
174
|
+
// alone, where the handlers look more like an application's, the share that calls something
|
|
175
|
+
// rises to 59% and the two would unlock nothing at all. What keeps a route off this path is
|
|
176
|
+
// that its answer is not knowable until the request arrives, and a variable is only another
|
|
177
|
+
// way to spell an answer that already was.
|
|
169
178
|
/** @type {any[]} */
|
|
170
179
|
const tokens = [...acorn.tokenizer(code, { ecmaVersion: "latest" })];
|
|
171
180
|
|
package/src/node-shim.js
CHANGED
|
@@ -24,6 +24,9 @@ limitations under the License.
|
|
|
24
24
|
|
|
25
25
|
const { IncomingMessage } = require("http");
|
|
26
26
|
|
|
27
|
+
/** What µWS returns for an address nobody declared. */
|
|
28
|
+
const emptyAddress = new ArrayBuffer(0);
|
|
29
|
+
|
|
27
30
|
/**
|
|
28
31
|
* An IP address as the four or sixteen bytes uWS hands over, since that is what req.ip parses.
|
|
29
32
|
* Anything unreadable comes back empty, which req.ip reports as undefined, the same answer it gives
|
|
@@ -370,6 +373,14 @@ class NodeHttpResponse {
|
|
|
370
373
|
return Buffer.from(this._nodeReq.socket?.remoteAddress || "");
|
|
371
374
|
}
|
|
372
375
|
|
|
376
|
+
/**
|
|
377
|
+
* Always empty: node's server does not read the PROXY protocol, so a request that arrived
|
|
378
|
+
* through it never carries an address a proxy declared, whatever "trust proxy protocol" says.
|
|
379
|
+
*/
|
|
380
|
+
getProxiedRemoteAddress() {
|
|
381
|
+
return emptyAddress;
|
|
382
|
+
}
|
|
383
|
+
|
|
373
384
|
/** The client port, or 0 when the socket has already gone. */
|
|
374
385
|
getRemotePort() {
|
|
375
386
|
return this._nodeReq.socket?.remotePort ?? 0;
|
package/src/request.js
CHANGED
|
@@ -125,6 +125,9 @@ function mapsIPv4Peer(app) {
|
|
|
125
125
|
return !(host && isIP(host) === 4);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** What µWS returns for a proxied address when no PROXY protocol preamble arrived. */
|
|
129
|
+
const emptyAddress = new ArrayBuffer(0);
|
|
130
|
+
|
|
128
131
|
const discardedDuplicates = new Set([
|
|
129
132
|
"age",
|
|
130
133
|
"authorization",
|
|
@@ -146,8 +149,6 @@ const discardedDuplicates = new Set([
|
|
|
146
149
|
"user-agent"
|
|
147
150
|
]);
|
|
148
151
|
|
|
149
|
-
let key = 0;
|
|
150
|
-
|
|
151
152
|
// 128 KB of body buffered before uWS is asked to pause
|
|
152
153
|
const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
153
154
|
|
|
@@ -265,9 +266,6 @@ Object.defineProperty(LazyReadableBase.prototype, "readable", {
|
|
|
265
266
|
});
|
|
266
267
|
|
|
267
268
|
module.exports = class Request extends LazyReadable {
|
|
268
|
-
/** @type {Record<string, any>|null} */
|
|
269
|
-
#cachedQuery = null;
|
|
270
|
-
|
|
271
269
|
/** @type {Record<string, any>|null} */
|
|
272
270
|
#cachedHeaders = null;
|
|
273
271
|
|
|
@@ -418,6 +416,13 @@ module.exports = class Request extends LazyReadable {
|
|
|
418
416
|
*/
|
|
419
417
|
rawIp;
|
|
420
418
|
|
|
419
|
+
/**
|
|
420
|
+
* Whether rawIp came from a PROXY protocol preamble rather than from the socket. Only the
|
|
421
|
+
* IPv4 mapping reads it, see parsedIp. Declared for the same reason as rawIp.
|
|
422
|
+
* @type {boolean}
|
|
423
|
+
*/
|
|
424
|
+
_ipFromProxy = false;
|
|
425
|
+
|
|
421
426
|
/**
|
|
422
427
|
* Whether the request declared a body, content-length or transfer-encoding, spotted during
|
|
423
428
|
* the header copy. Declared for the same reason as rawIp.
|
|
@@ -489,6 +494,14 @@ module.exports = class Request extends LazyReadable {
|
|
|
489
494
|
// framework itself: body framing, keep-alive, and accept for the error page a
|
|
490
495
|
// throw could still need. A GET that does declare a body is the rare case, and
|
|
491
496
|
// the parsers and the stream want the whole picture, so it takes the full copy.
|
|
497
|
+
//
|
|
498
|
+
// Seven named reads against one forEach looks like it should lose, and does not: the
|
|
499
|
+
// seven are flat at 0.75us however many headers are on the wire, since each one is a
|
|
500
|
+
// napi crossing and the scan behind it is nothing, while the copy pays a hop back into
|
|
501
|
+
// JS per header and grows, 1.16us at four headers, 1.61 at eight, 2.90 at sixteen. They
|
|
502
|
+
// do not cross, and the gap widens exactly where real traffic lives, since a browser
|
|
503
|
+
// sends a dozen or more. The body case pays two of the seven and then copies anyway,
|
|
504
|
+
// which is 0.2us on a request that is about to read a body.
|
|
492
505
|
const length = req.getHeader("content-length");
|
|
493
506
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
494
507
|
if (length !== "" || transferEncoding !== "") {
|
|
@@ -533,10 +546,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
533
546
|
currentRequest = null;
|
|
534
547
|
}
|
|
535
548
|
this.routeCount = 1;
|
|
536
|
-
this.key = key++;
|
|
537
|
-
if (key > 100000) {
|
|
538
|
-
key = 0;
|
|
539
|
-
}
|
|
540
549
|
this.app = app;
|
|
541
550
|
// both forms are kept, because both are asked for: the query with its "?" goes into
|
|
542
551
|
// req.url, and req.query parses the raw one. Keeping only the first meant slicing the "?"
|
|
@@ -608,10 +617,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
608
617
|
this._appStack = undefined;
|
|
609
618
|
this.receivedData = false;
|
|
610
619
|
// reading ip is very slow in UWS, so its better to not do it unless truly needed
|
|
611
|
-
if (
|
|
612
|
-
//
|
|
613
|
-
//
|
|
614
|
-
this.rawIp = this.
|
|
620
|
+
if (app.needsIpAfterResponse) {
|
|
621
|
+
// an app that has been seen asking after the response reads it now, because by then
|
|
622
|
+
// µWS has freed it
|
|
623
|
+
this.rawIp = this._readRawIp();
|
|
624
|
+
} else if (app._ipProbes < 100) {
|
|
625
|
+
// and until this app has been seen either way, the first hundred requests read it, so
|
|
626
|
+
// one of them can be the one that finds out
|
|
627
|
+
app._ipProbes++;
|
|
628
|
+
this.rawIp = this._readRawIp();
|
|
615
629
|
}
|
|
616
630
|
|
|
617
631
|
// A body exists on the wire only when the request declares one, content-length or
|
|
@@ -875,7 +889,6 @@ module.exports = class Request extends LazyReadable {
|
|
|
875
889
|
this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
|
|
876
890
|
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
877
891
|
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
878
|
-
this.#cachedQuery = null;
|
|
879
892
|
this._originalPath = prefix + newPath;
|
|
880
893
|
this.path = newPath;
|
|
881
894
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
@@ -884,27 +897,40 @@ module.exports = class Request extends LazyReadable {
|
|
|
884
897
|
}
|
|
885
898
|
|
|
886
899
|
/**
|
|
887
|
-
* The query string parsed by whichever parser the "query parser" setting names
|
|
888
|
-
*
|
|
889
|
-
*
|
|
900
|
+
* The query string parsed by whichever parser the "query parser" setting names. A null-prototype
|
|
901
|
+
* object, so a key like "__proto__" cannot reach Object.prototype. No setter, so assigning to
|
|
902
|
+
* req.query throws as it does on Express.
|
|
903
|
+
*
|
|
904
|
+
* Every read answers a new object, because express's getter re-parses on every read and so hands
|
|
905
|
+
* one back too. Two consequences an application can see, and both of them bite: `req.query` is
|
|
906
|
+
* never the object another reader holds, and a write to a key of it is gone by the next read.
|
|
907
|
+
* That second one is how express-validator's sanitisers behave: `.trim()` on a query parameter
|
|
908
|
+
* changes nothing an ordinary handler will see, which is why it also offers matchedData(). With
|
|
909
|
+
* the parse cached and handed out as itself, the sanitised value leaked into req.query here and
|
|
910
|
+
* a handler written against express read a trimmed value where express gives it the raw one.
|
|
911
|
+
*
|
|
912
|
+
* And that is why there is no cache: the fresh object comes from parsing the raw string again,
|
|
913
|
+
* not from copying a kept parse. As first shipped this was parse-once-copy-per-read, and the
|
|
914
|
+
* copy was the expensive half: Object.assign between null-prototype objects, which live in
|
|
915
|
+
* V8's dictionary mode, measured 638ns for a two-parameter query where parsing the same string
|
|
916
|
+
* measures 119ns, and on a benchmark whose every request carries such a query it cost +1.5us
|
|
917
|
+
* of CPU per request, which a public arena saw as -8% on its query-carrying rows. A handler
|
|
918
|
+
* that reads req.query once per request now pays exactly what it paid when the parse was
|
|
919
|
+
* cached, one parse, and a handler that reads it N times pays N parses, which is express's
|
|
920
|
+
* own cost shape.
|
|
890
921
|
*
|
|
891
922
|
* @returns {Record<string, any>}
|
|
892
923
|
*/
|
|
893
924
|
get query() {
|
|
894
|
-
if (this.#cachedQuery) {
|
|
895
|
-
return this.#cachedQuery;
|
|
896
|
-
}
|
|
897
925
|
const qp = this.app.get("query parser fn");
|
|
898
926
|
// the vendored default already answers on a bare null prototype, so it goes out as is;
|
|
899
927
|
// any other parser is copied onto one, which is what kept fast-querystring's result from
|
|
900
928
|
// inspecting as "Empty <[Object: null prototype] {}>" where Express shows the bare form
|
|
901
|
-
|
|
929
|
+
return qp
|
|
902
930
|
? qp === parseQuery
|
|
903
931
|
? parseQuery(this._rawQuery)
|
|
904
932
|
: Object.assign(Object.create(null), qp(this._rawQuery))
|
|
905
933
|
: Object.create(null);
|
|
906
|
-
this.#cachedQuery = parsed;
|
|
907
|
-
return parsed;
|
|
908
934
|
}
|
|
909
935
|
|
|
910
936
|
/**
|
|
@@ -949,6 +975,32 @@ module.exports = class Request extends LazyReadable {
|
|
|
949
975
|
return typeof val === "string" && val.toLowerCase() === "xmlhttprequest";
|
|
950
976
|
}
|
|
951
977
|
|
|
978
|
+
/**
|
|
979
|
+
* The peer address bytes, from the socket or, when the application asked for it, from a PROXY
|
|
980
|
+
* protocol preamble the load balancer in front of this server sent ahead of the request.
|
|
981
|
+
*
|
|
982
|
+
* The setting is off by default and has to stay that way. µWS parses the preamble from whoever
|
|
983
|
+
* sends it, with nothing to ask for it at listen time and no way to restrict who may, so an
|
|
984
|
+
* application that took the address unconditionally would let any client claim any address:
|
|
985
|
+
* the first sixteen bytes of a connection are enough to become 10.0.0.1 for a rate limiter, an
|
|
986
|
+
* allow list or an audit log. Turn it on only when nothing can reach this server except the
|
|
987
|
+
* proxy in front of it.
|
|
988
|
+
*
|
|
989
|
+
* @returns {ArrayBuffer} the socket's own address when no preamble arrived
|
|
990
|
+
*/
|
|
991
|
+
_readRawIp() {
|
|
992
|
+
const uwsRes = this._res;
|
|
993
|
+
if (this.app.get("trust proxy protocol")) {
|
|
994
|
+
const proxied = uwsRes.getProxiedRemoteAddress();
|
|
995
|
+
// empty unless a preamble arrived, which is the only thing that tells the two apart
|
|
996
|
+
if (proxied.byteLength !== 0) {
|
|
997
|
+
this._ipFromProxy = true;
|
|
998
|
+
return proxied;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
return uwsRes.getRemoteAddress();
|
|
1002
|
+
}
|
|
1003
|
+
|
|
952
1004
|
/**
|
|
953
1005
|
* The peer address as text, read from uWS and cached. Reading it is expensive and it is gone
|
|
954
1006
|
* once the response has finished, so it is read up front for the first hundred requests, and
|
|
@@ -971,7 +1023,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
971
1023
|
// fallback once
|
|
972
1024
|
return mapsIPv4Peer(this.app) ? "::ffff:127.0.0.1" : "127.0.0.1";
|
|
973
1025
|
}
|
|
974
|
-
this.rawIp = this.
|
|
1026
|
+
this.rawIp = this._readRawIp();
|
|
975
1027
|
}
|
|
976
1028
|
// read once: the branch above settled it, and every use below wants the bytes
|
|
977
1029
|
const rawIp = /** @type {ArrayBuffer} */ (this.rawIp);
|
|
@@ -980,7 +1032,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
980
1032
|
if (rawIp.byteLength === 4) {
|
|
981
1033
|
// ipv4
|
|
982
1034
|
ip = new Uint8Array(rawIp).join(".");
|
|
983
|
-
|
|
1035
|
+
// the mapped form belongs to a dual stack listener, which is what makes an IPv4 peer
|
|
1036
|
+
// arrive as ::ffff:a.b.c.d. An address a proxy declared never came through that socket,
|
|
1037
|
+
// so it is left as the four numbers the proxy sent
|
|
1038
|
+
if (!this._ipFromProxy && mapsIPv4Peer(this.app)) {
|
|
984
1039
|
ip = "::ffff:" + ip;
|
|
985
1040
|
}
|
|
986
1041
|
} else if (rawIp.byteLength === 16) {
|
|
@@ -1053,12 +1108,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
1053
1108
|
_detachFromResponse() {
|
|
1054
1109
|
const uwsRes = this._res;
|
|
1055
1110
|
if (!this.rawIp) {
|
|
1056
|
-
this.rawIp =
|
|
1111
|
+
this.rawIp = this._readRawIp();
|
|
1057
1112
|
}
|
|
1058
1113
|
const remotePort = uwsRes.getRemotePort();
|
|
1059
1114
|
const rawIp = this.rawIp;
|
|
1060
1115
|
this._res = {
|
|
1061
1116
|
getRemoteAddress: () => rawIp,
|
|
1117
|
+
// whatever a preamble said is already in rawIp, and asking again is the use after free
|
|
1118
|
+
// this method exists to avoid
|
|
1119
|
+
getProxiedRemoteAddress: () => emptyAddress,
|
|
1062
1120
|
getRemotePort: () => remotePort,
|
|
1063
1121
|
// a body cannot arrive on an upgraded socket, and a stray reader must not reach µWS
|
|
1064
1122
|
onData() {},
|
package/src/response.js
CHANGED
|
@@ -121,7 +121,84 @@ function statusLine(code, text) {
|
|
|
121
121
|
return `${code} ${text ?? statuses.message[code] ?? "unknown"}`.trim();
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
124
|
+
/**
|
|
125
|
+
* A Writable that has not built its state yet, the mirror of LazyReadable in request.js and there
|
|
126
|
+
* for the same reason: a response is a Writable because middleware expects one, and the ordinary
|
|
127
|
+
* one never uses it. `send()` reaches `end()`, which is overridden here and goes straight to
|
|
128
|
+
* _finish, so the WritableState is allocated for every response and read by nobody. It is needed
|
|
129
|
+
* only by res.write(), by a stream piped into the response, by cork and by the writableX getters.
|
|
130
|
+
*
|
|
131
|
+
* `Response extends LazyWritable`, whose prototype is Writable's, so `res instanceof Writable`
|
|
132
|
+
* stays true and every Writable method is reachable; what is missing is `_writableState`, built on
|
|
133
|
+
* the first touch. Measured at 45 nanoseconds a response on the machine this was written on.
|
|
134
|
+
*
|
|
135
|
+
* As on the Readable side the wrapping is generated rather than written out: every own member of
|
|
136
|
+
* Writable's prototype gets a version that materialises first, so there is no list to keep in step.
|
|
137
|
+
* Missing one would not be a slow path, it would be a TypeError on `undefined._writableState`.
|
|
138
|
+
*/
|
|
139
|
+
class LazyWritableBase {}
|
|
140
|
+
Object.setPrototypeOf(LazyWritableBase.prototype, Writable.prototype);
|
|
141
|
+
Object.setPrototypeOf(LazyWritableBase, Writable);
|
|
142
|
+
|
|
143
|
+
// what the chain says at runtime, said again for the type checker, which cannot see a prototype
|
|
144
|
+
// being reassigned
|
|
145
|
+
const LazyWritable = /** @type {typeof Writable} */ (/** @type {unknown} */ (LazyWritableBase));
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Builds the stream this object has been pretending to be. Idempotent: everything reachable from
|
|
149
|
+
* outside goes through it, so it is called far more often than it does anything.
|
|
150
|
+
*
|
|
151
|
+
* EventEmitter's init keeps an _events that is already there, so both the shape the constructor
|
|
152
|
+
* wrote and any listener added before this survive it.
|
|
153
|
+
*
|
|
154
|
+
* @param {any} stream
|
|
155
|
+
*/
|
|
156
|
+
function materialiseWritable(stream) {
|
|
157
|
+
if (stream._writableState === undefined) {
|
|
158
|
+
Writable.call(stream);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
for (const member of [
|
|
163
|
+
...Object.getOwnPropertyNames(Writable.prototype),
|
|
164
|
+
...Object.getOwnPropertySymbols(Writable.prototype)
|
|
165
|
+
]) {
|
|
166
|
+
if (member === "constructor") {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Writable.prototype, member));
|
|
170
|
+
if (typeof descriptor.value === "function") {
|
|
171
|
+
const inner = descriptor.value;
|
|
172
|
+
Object.defineProperty(LazyWritableBase.prototype, member, {
|
|
173
|
+
...descriptor,
|
|
174
|
+
/** @this {any} @param {...any} args */
|
|
175
|
+
value: function (...args) {
|
|
176
|
+
materialiseWritable(this);
|
|
177
|
+
return inner.apply(this, args);
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
} else if (descriptor.get || descriptor.set) {
|
|
181
|
+
const innerGet = descriptor.get;
|
|
182
|
+
const innerSet = descriptor.set;
|
|
183
|
+
Object.defineProperty(LazyWritableBase.prototype, member, {
|
|
184
|
+
...descriptor,
|
|
185
|
+
get: innerGet
|
|
186
|
+
? /** @this {any} */ function () {
|
|
187
|
+
materialiseWritable(this);
|
|
188
|
+
return innerGet.call(this);
|
|
189
|
+
}
|
|
190
|
+
: undefined,
|
|
191
|
+
set: innerSet
|
|
192
|
+
? /** @this {any} @param {any} value */ function (value) {
|
|
193
|
+
materialiseWritable(this);
|
|
194
|
+
innerSet.call(this, value);
|
|
195
|
+
}
|
|
196
|
+
: undefined
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = class Response extends LazyWritable {
|
|
125
202
|
/** @type {Socket|null} */
|
|
126
203
|
#socket = null;
|
|
127
204
|
|
|
@@ -151,6 +228,19 @@ module.exports = class Response extends Writable {
|
|
|
151
228
|
*/
|
|
152
229
|
constructor(res, req, app) {
|
|
153
230
|
super();
|
|
231
|
+
// the EventEmitter half stays eager, since the stream half is what LazyWritable defers and
|
|
232
|
+
// the two listeners below are written straight into this map. These are the five keys and
|
|
233
|
+
// the order node's own Writable constructor lays down, so the hidden class is the one every
|
|
234
|
+
// other stream in the process has, and node's init keeps this object when the state is
|
|
235
|
+
// finally built
|
|
236
|
+
this._events = {
|
|
237
|
+
close: undefined,
|
|
238
|
+
error: undefined,
|
|
239
|
+
prefinish: undefined,
|
|
240
|
+
finish: undefined,
|
|
241
|
+
drain: undefined
|
|
242
|
+
};
|
|
243
|
+
this._eventsCount = 0;
|
|
154
244
|
this._req = req;
|
|
155
245
|
// linked here rather than by the caller: the pair is built together, and a field the
|
|
156
246
|
// constructor leaves unset is a shape change on whoever assigns it first
|
|
@@ -185,6 +275,10 @@ module.exports = class Response extends Writable {
|
|
|
185
275
|
}
|
|
186
276
|
|
|
187
277
|
this.body = undefined;
|
|
278
|
+
// what was handed to uWS, kept so a caller asking for content-length after the fact can be
|
|
279
|
+
// answered, see get(). Undefined until the response ends, and for one that sends no body
|
|
280
|
+
/** @type {string|Buffer|undefined} */
|
|
281
|
+
this._sentBody = undefined;
|
|
188
282
|
// false while the uWS route handler is still in its synchronous window, where uWS holds
|
|
189
283
|
// the socket corked itself; the two uWS entry points flip it once that window closes
|
|
190
284
|
this._corkNeeded = false;
|
|
@@ -448,6 +542,12 @@ module.exports = class Response extends Writable {
|
|
|
448
542
|
// the value here is nearly always the 10-char "keep-alive", which paid a scan per response
|
|
449
543
|
const closing =
|
|
450
544
|
typeof connection === "string" && connection.length === 5 && connection.toLowerCase() === "close";
|
|
545
|
+
// for..in over an object some responses delete from, which is the shape the request side
|
|
546
|
+
// was taken off for #rawHeadersEntries. It stays here, and the difference is where the
|
|
547
|
+
// deletes are: a 200 with a body performs none. Only 204, 304, 205, the freshness branch of
|
|
548
|
+
// sendFile and removeHeader do, this object is built fresh per response, so a dictionary one
|
|
549
|
+
// of them made costs that response and nothing after it. The request side deleted on every
|
|
550
|
+
// request, which is what made it worth a different structure
|
|
451
551
|
for (const header in headers) {
|
|
452
552
|
if (closing && header === "keep-alive") {
|
|
453
553
|
continue;
|
|
@@ -584,8 +684,12 @@ module.exports = class Response extends Writable {
|
|
|
584
684
|
// an allocation per body, and uWS reads the view's own offset and length
|
|
585
685
|
if (this.req.method === "HEAD") {
|
|
586
686
|
const length = Buffer.byteLength(data ?? "");
|
|
687
|
+
this.headers["content-length"] = String(length);
|
|
587
688
|
this._res.endWithoutBody(length.toString());
|
|
588
689
|
} else {
|
|
690
|
+
// remembered rather than measured: only a caller that asks for content-length pays
|
|
691
|
+
// for it, and uWS is measuring the same bytes for the wire anyway
|
|
692
|
+
this._sentBody = data ?? "";
|
|
589
693
|
this._res.end(data);
|
|
590
694
|
}
|
|
591
695
|
}
|
|
@@ -1185,7 +1289,19 @@ module.exports = class Response extends Writable {
|
|
|
1185
1289
|
* @returns {string|string[]|undefined}
|
|
1186
1290
|
*/
|
|
1187
1291
|
get(field) {
|
|
1188
|
-
|
|
1292
|
+
const name = field.toLowerCase();
|
|
1293
|
+
const value = this.headers[name];
|
|
1294
|
+
// Content-Length is on the wire but not in here: uWS measures the body it is handed and
|
|
1295
|
+
// writes the header itself, which saves measuring it twice. Express sets it in send(), so
|
|
1296
|
+
// anything reading it back finds it there, and morgan's common and combined formats do
|
|
1297
|
+
// exactly that on every line they write. Worked out here rather than in send() so a
|
|
1298
|
+
// response nobody asks pays nothing, and kept once worked out.
|
|
1299
|
+
if (value === undefined && name === "content-length" && this._sentBody !== undefined) {
|
|
1300
|
+
const length = Buffer.byteLength(this._sentBody);
|
|
1301
|
+
this.headers["content-length"] = String(length);
|
|
1302
|
+
return String(length);
|
|
1303
|
+
}
|
|
1304
|
+
return value;
|
|
1189
1305
|
}
|
|
1190
1306
|
|
|
1191
1307
|
/**
|
|
@@ -1362,23 +1478,22 @@ module.exports = class Response extends Writable {
|
|
|
1362
1478
|
|
|
1363
1479
|
this.vary("Accept");
|
|
1364
1480
|
|
|
1365
|
-
//
|
|
1366
|
-
//
|
|
1367
|
-
//
|
|
1368
|
-
//
|
|
1369
|
-
|
|
1370
|
-
// not identical. The whole thing is the open req.next question at Walk's constructor
|
|
1481
|
+
// the router next, as express hands over: inside a route with a four argument handler of
|
|
1482
|
+
// its own, a 406 leaves the route rather than being caught by that handler. Where the two
|
|
1483
|
+
// are the same step, _leaveRoute is the very object the surrounding layer received, which
|
|
1484
|
+
// is what express's own test asserts, see Walk#runRoute
|
|
1485
|
+
const next = this.req._leaveRoute ?? this.req.next;
|
|
1371
1486
|
if (key) {
|
|
1372
1487
|
this.set("Content-Type", normalizeType(key).value);
|
|
1373
|
-
object[key](this.req, this,
|
|
1488
|
+
object[key](this.req, this, next);
|
|
1374
1489
|
} else if (object.default) {
|
|
1375
|
-
object.default(this.req, this,
|
|
1490
|
+
object.default(this.req, this, next);
|
|
1376
1491
|
} else {
|
|
1377
1492
|
// an error and not an answer: express hands the error handler the types it could have
|
|
1378
1493
|
// sent, which is how an application says what it supports
|
|
1379
1494
|
const err = httpError(406);
|
|
1380
1495
|
err.types = keys.map((type) => normalizeType(type).value);
|
|
1381
|
-
|
|
1496
|
+
next(err);
|
|
1382
1497
|
}
|
|
1383
1498
|
|
|
1384
1499
|
return this;
|
package/src/router.js
CHANGED
|
@@ -333,7 +333,11 @@ class Walk {
|
|
|
333
333
|
}
|
|
334
334
|
}
|
|
335
335
|
req.next = this.next;
|
|
336
|
-
|
|
336
|
+
// the same step when the route has one callback, and then it has to be the same object:
|
|
337
|
+
// express hands res.format's handlers the next its own layer received, and its test asserts
|
|
338
|
+
// that identity. With more than one callback the two differ for real, and what express
|
|
339
|
+
// hands over is the one that leaves the route
|
|
340
|
+
req._leaveRoute = route.callbacks.length > 1 ? this.leaveRoute : this.next;
|
|
337
341
|
if (continueRoute === "route") {
|
|
338
342
|
this.step("route");
|
|
339
343
|
} else if (continueRoute) {
|
|
@@ -920,6 +924,16 @@ const BODY_METHODS = new Set(["POST", "PUT", "PATCH", "QUERY"]);
|
|
|
920
924
|
* written on, and the parser prologue it reaches measured 38. Ten to one, for a layer that had
|
|
921
925
|
* nothing to do.
|
|
922
926
|
*
|
|
927
|
+
* That number is also why fusing consecutive layers into one generated function keeps coming up,
|
|
928
|
+
* and why it is not here. Counted over a real front, morgan, helmet, compression, cors, the two body
|
|
929
|
+
* parsers, express-session, a middleware of one's own and express.static: three of the nine can be
|
|
930
|
+
* fused, and the longest run of fusable ones in a row is one. Fusing needs two. The rule was relaxed
|
|
931
|
+
* from "calls next once, unconditionally" to merely "calls next synchronously" and the answer did
|
|
932
|
+
* not move, because the six that fail all call next from inside a callback: they are asynchronous by
|
|
933
|
+
* nature, reading a body, stat-ing a file, loading a session. A layer that has not decided by the
|
|
934
|
+
* time it returns cannot be fused by any design that keeps the semantics. What fuses is a run of
|
|
935
|
+
* trivial middlewares, which is a benchmark shape rather than an application's.
|
|
936
|
+
*
|
|
923
937
|
* @param {any} route
|
|
924
938
|
* @param {any} req
|
|
925
939
|
* @returns {boolean}
|
|
@@ -956,6 +970,68 @@ function couldAnswer(route, path) {
|
|
|
956
970
|
return route.pattern === path;
|
|
957
971
|
}
|
|
958
972
|
|
|
973
|
+
/**
|
|
974
|
+
* Whether a layer written before a mount could answer a request for one of the paths inside it.
|
|
975
|
+
*
|
|
976
|
+
* A mount covers everything under its path, so this is a question about a subtree rather than about
|
|
977
|
+
* the mount point, and the two answers differ: `/a` and `/:p0/:p1/:p2` match none of each other's
|
|
978
|
+
* text, and both answer `/a/x/y`. µWS jumps straight to whichever leaf it registered, so a leaf a
|
|
979
|
+
* layer like this could have answered has to stay on the generic path, which is the only place
|
|
980
|
+
* express's registration order decides.
|
|
981
|
+
*
|
|
982
|
+
* Only layers with more segments than the mount path reach this: one with as few already matches
|
|
983
|
+
* the mount point itself, and _optimizeRoute has refused the mount before the walk gets here.
|
|
984
|
+
*
|
|
985
|
+
* Compared folded whichever way the routers are set. A wrong yes costs a leaf its native
|
|
986
|
+
* registration and nothing else.
|
|
987
|
+
*
|
|
988
|
+
* @param {{path: string, use: boolean, method: string, all: boolean}} guard
|
|
989
|
+
* @param {string} leafPath the leaf's absolute path, parameters and all
|
|
990
|
+
* @param {any} leaf
|
|
991
|
+
* @returns {boolean}
|
|
992
|
+
*/
|
|
993
|
+
function shadowsLeaf(guard, leafPath, leaf) {
|
|
994
|
+
if (!guard.all && guard.method !== leaf.method && !(guard.method === "HEAD" && leaf.method === "GET")) {
|
|
995
|
+
return false;
|
|
996
|
+
}
|
|
997
|
+
return pathsCanOverlap(guard.path.toLowerCase(), leafPath.toLowerCase(), guard.use);
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* The layers before a mount that answer some of what is inside it and not all of it, which is the
|
|
1002
|
+
* one thing neither the chain nor µWS's own choice can say: the chain runs what is in it without
|
|
1003
|
+
* matching again, and µWS picks by specificity. They are carried down the walk instead and asked
|
|
1004
|
+
* about every leaf, see shadowsLeaf.
|
|
1005
|
+
*
|
|
1006
|
+
* @param {any} router the router the mount belongs to
|
|
1007
|
+
* @param {any} mount
|
|
1008
|
+
* @param {string} pathPrefix what the mounts above this one consumed
|
|
1009
|
+
* @param {any[]} chain the layers that always run before the mount, which need no guard
|
|
1010
|
+
* @param {any[]} inherited the guards from further out, since a mount two levels down is under
|
|
1011
|
+
* everything written before either of them
|
|
1012
|
+
* @returns {any[]|null} null when a path cannot be read segment by segment, which leaves the mount
|
|
1013
|
+
* to ordinary dispatch rather than guessing about it
|
|
1014
|
+
*/
|
|
1015
|
+
function guardsInside(router, mount, pathPrefix, chain, inherited) {
|
|
1016
|
+
let guards = inherited;
|
|
1017
|
+
for (const r of router._routes) {
|
|
1018
|
+
if (r.routeKey > mount.routeKey) {
|
|
1019
|
+
break;
|
|
1020
|
+
}
|
|
1021
|
+
if (r === mount || chain.includes(r)) {
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (typeof r.path !== "string") {
|
|
1025
|
+
return null;
|
|
1026
|
+
}
|
|
1027
|
+
if (guards === inherited) {
|
|
1028
|
+
guards = [...inherited];
|
|
1029
|
+
}
|
|
1030
|
+
guards.push({ path: pathPrefix + r.path, use: r.use === true, method: r.method, all: r.all === true });
|
|
1031
|
+
}
|
|
1032
|
+
return guards;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
959
1035
|
/**
|
|
960
1036
|
* Notes which application is current before a mounted one is entered, so that exact one comes back
|
|
961
1037
|
* when it hands over.
|
|
@@ -1146,6 +1222,26 @@ module.exports = class Router extends EventEmitter {
|
|
|
1146
1222
|
*/
|
|
1147
1223
|
_isApplication = false;
|
|
1148
1224
|
|
|
1225
|
+
/**
|
|
1226
|
+
* Whether anything served from here has been seen reading req.ip after the response, by which
|
|
1227
|
+
* point µWS has freed the address. Set once, from Request#parsedIp, and read on every request
|
|
1228
|
+
* after that. Here rather than on Application because a plain Router serves requests of its own
|
|
1229
|
+
* through the node shim.
|
|
1230
|
+
*
|
|
1231
|
+
* @type {boolean}
|
|
1232
|
+
*/
|
|
1233
|
+
needsIpAfterResponse = false;
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* How many requests still read the peer address up front whether or not anyone asks, so that
|
|
1237
|
+
* one of them can be the one that finds out. Counts to a hundred and stops: it used to be read
|
|
1238
|
+
* off a module-wide counter that wrapped at 100000, so the window reopened every time it did
|
|
1239
|
+
* and a hundred requests paid again for a discovery made long before.
|
|
1240
|
+
*
|
|
1241
|
+
* @type {number}
|
|
1242
|
+
*/
|
|
1243
|
+
_ipProbes = 0;
|
|
1244
|
+
|
|
1149
1245
|
/**
|
|
1150
1246
|
* The two routing flags once read, undefined until then. Express passes caseSensitive and
|
|
1151
1247
|
* strict in when it builds a router and never looks at them again, so they are frozen here at
|
|
@@ -1701,8 +1797,9 @@ module.exports = class Router extends EventEmitter {
|
|
|
1701
1797
|
return;
|
|
1702
1798
|
}
|
|
1703
1799
|
|
|
1704
|
-
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts
|
|
1705
|
-
|
|
1800
|
+
// pathPrefix/chainPrefix accumulate across nested sole-callback mounts, and outerGuards
|
|
1801
|
+
// carries what was written before them and answers only part of what is under them
|
|
1802
|
+
const walk = (router, pathPrefix, chainPrefix, outerGuards) => {
|
|
1706
1803
|
for (const route of router._routes) {
|
|
1707
1804
|
if (route.use) {
|
|
1708
1805
|
// only sole-callback mounts. Case rules do not gate the walk: each level's
|
|
@@ -1721,23 +1818,33 @@ module.exports = class Router extends EventEmitter {
|
|
|
1721
1818
|
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
1722
1819
|
continue;
|
|
1723
1820
|
}
|
|
1724
|
-
route._walkedInto = true;
|
|
1725
1821
|
pathToMount = pathToMount.slice(0, -1);
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1822
|
+
const guards = guardsInside(router, route, pathPrefix, pathToMount, outerGuards);
|
|
1823
|
+
if (guards === null) {
|
|
1824
|
+
route._whyGeneric = "a path written before it cannot be read segment by segment";
|
|
1825
|
+
continue;
|
|
1826
|
+
}
|
|
1827
|
+
route._walkedInto = true;
|
|
1828
|
+
walk(
|
|
1829
|
+
route.callbacks[0],
|
|
1830
|
+
pathPrefix + route.path,
|
|
1831
|
+
[
|
|
1832
|
+
...chainPrefix,
|
|
1833
|
+
...pathToMount,
|
|
1834
|
+
{
|
|
1835
|
+
...route,
|
|
1836
|
+
callbacks: [],
|
|
1837
|
+
callbackKinds: [],
|
|
1838
|
+
keepMount: true,
|
|
1839
|
+
// mounted sub-apps become req.app during their dispatch, like express
|
|
1840
|
+
mountApp:
|
|
1841
|
+
route.callbacks[0].constructor.name === "Application"
|
|
1842
|
+
? route.callbacks[0]
|
|
1843
|
+
: undefined
|
|
1844
|
+
}
|
|
1845
|
+
],
|
|
1846
|
+
guards
|
|
1847
|
+
);
|
|
1741
1848
|
} else {
|
|
1742
1849
|
// said once here rather than at each condition above: a mount is walked into
|
|
1743
1850
|
// only when µWS can match its path on its own and it carries exactly one
|
|
@@ -1757,6 +1864,16 @@ module.exports = class Router extends EventEmitter {
|
|
|
1757
1864
|
(!pathPrefix || !router._isFollowedByAnOverlap(route, router._routes)))) &&
|
|
1758
1865
|
supportedUwsMethods.has(route.method)
|
|
1759
1866
|
) {
|
|
1867
|
+
// something outside this router, written before the mount it is in, that could
|
|
1868
|
+
// answer this exact path. µWS would jump here and never give it its turn
|
|
1869
|
+
if (outerGuards.length > 0 && typeof route.path === "string") {
|
|
1870
|
+
const absolute = pathPrefix + route.path;
|
|
1871
|
+
const guard = outerGuards.find((g) => shadowsLeaf(g, absolute, route));
|
|
1872
|
+
if (guard) {
|
|
1873
|
+
route._whyGeneric = `${guard.path} is written before the mount it is in and answers the same paths`;
|
|
1874
|
+
continue;
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1760
1877
|
const leafPath = router._optimizeRoute(route, router._routes);
|
|
1761
1878
|
if (!leafPath) {
|
|
1762
1879
|
route._whyGeneric = "something before it in the same router overlaps its paths";
|
|
@@ -1816,7 +1933,7 @@ module.exports = class Router extends EventEmitter {
|
|
|
1816
1933
|
}
|
|
1817
1934
|
};
|
|
1818
1935
|
|
|
1819
|
-
walk(this, "", []);
|
|
1936
|
+
walk(this, "", [], []);
|
|
1820
1937
|
}
|
|
1821
1938
|
|
|
1822
1939
|
/**
|
package/src/utils.js
CHANGED
|
@@ -809,7 +809,11 @@ const defaultSettings = {
|
|
|
809
809
|
// "case sensitive routing" is deliberately absent: unset means insensitive, as in Express 5.
|
|
810
810
|
// The native µWS router matches bytes, so the compiler in _compileOptimizedRoutes only hands
|
|
811
811
|
// it routes whose earlier siblings it can prove agree under either case rule.
|
|
812
|
-
"declarative responses": true
|
|
812
|
+
"declarative responses": true,
|
|
813
|
+
// off, and it is a security setting rather than a compatibility one: with it on, req.ip is the
|
|
814
|
+
// address a PROXY protocol preamble declared. µWS reads that preamble from any client, so this
|
|
815
|
+
// belongs only to a server nothing can reach except the proxy in front of it. See Request#_readRawIp
|
|
816
|
+
"trust proxy protocol": false
|
|
813
817
|
};
|
|
814
818
|
|
|
815
819
|
/**
|