fulmine.js 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +29 -2
- package/README.md +176 -63
- package/package.json +11 -2
- package/src/application.js +67 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +28 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +67 -6
- package/src/node-shim.js +16 -3
- package/src/options.d.ts +16 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +365 -60
- package/src/response.js +172 -12
- package/src/router.js +767 -177
- package/src/types.d.ts +16 -0
- package/src/usage.js +16 -0
- package/src/utils.js +272 -37
- package/src/view.js +2 -0
- package/src/websocket.js +16 -0
- package/src/worker.js +2 -0
package/src/request.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Copyright 2024 dimden.dev
|
|
3
3
|
Copyright 2026 Nigro Simone
|
|
4
4
|
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
5
7
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
8
|
you may not use this file except in compliance with the License.
|
|
7
9
|
You may obtain a copy of the License at
|
|
@@ -84,6 +86,31 @@ function formatIPv6(groups) {
|
|
|
84
86
|
return out;
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Whether these sixteen bytes are an IPv4-mapped address, ::ffff:0:0/96: ten zero bytes and then
|
|
91
|
+
* 0xffff. Ten comparisons rather than a loop, because this runs on every address that is read and
|
|
92
|
+
* the first mismatch answers immediately for a real IPv6 peer.
|
|
93
|
+
*
|
|
94
|
+
* @param {Uint8Array} bytes exactly sixteen of them
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
function isMappedIPv4(bytes) {
|
|
98
|
+
return (
|
|
99
|
+
bytes[10] === 0xff &&
|
|
100
|
+
bytes[11] === 0xff &&
|
|
101
|
+
bytes[0] === 0 &&
|
|
102
|
+
bytes[1] === 0 &&
|
|
103
|
+
bytes[2] === 0 &&
|
|
104
|
+
bytes[3] === 0 &&
|
|
105
|
+
bytes[4] === 0 &&
|
|
106
|
+
bytes[5] === 0 &&
|
|
107
|
+
bytes[6] === 0 &&
|
|
108
|
+
bytes[7] === 0 &&
|
|
109
|
+
bytes[8] === 0 &&
|
|
110
|
+
bytes[9] === 0
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
87
114
|
/**
|
|
88
115
|
* Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps
|
|
89
116
|
* it whenever the listener is dual stack, which is every listen() not given an IPv4 address to
|
|
@@ -98,6 +125,9 @@ function mapsIPv4Peer(app) {
|
|
|
98
125
|
return !(host && isIP(host) === 4);
|
|
99
126
|
}
|
|
100
127
|
|
|
128
|
+
/** What µWS returns for a proxied address when no PROXY protocol preamble arrived. */
|
|
129
|
+
const emptyAddress = new ArrayBuffer(0);
|
|
130
|
+
|
|
101
131
|
const discardedDuplicates = new Set([
|
|
102
132
|
"age",
|
|
103
133
|
"authorization",
|
|
@@ -119,8 +149,6 @@ const discardedDuplicates = new Set([
|
|
|
119
149
|
"user-agent"
|
|
120
150
|
]);
|
|
121
151
|
|
|
122
|
-
let key = 0;
|
|
123
|
-
|
|
124
152
|
// 128 KB of body buffered before uWS is asked to pause
|
|
125
153
|
const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
126
154
|
|
|
@@ -129,7 +157,115 @@ const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
|
129
157
|
// of once per request.
|
|
130
158
|
let currentRequest = null;
|
|
131
159
|
|
|
132
|
-
|
|
160
|
+
/**
|
|
161
|
+
* A Readable that has not been built yet.
|
|
162
|
+
*
|
|
163
|
+
* Every request pays for the stream and almost none of them use it: a GET carries no body, and the
|
|
164
|
+
* bodies that do arrive are collected by µWS and handed to the parsers without the stream being
|
|
165
|
+
* touched. Measured on this machine, running Readable's constructor costs about 90ns of the 900ns
|
|
166
|
+
* a hello-world request costs in total, which is a tenth of it for a facility nobody asked for.
|
|
167
|
+
*
|
|
168
|
+
* So the chain says Readable and the constructor does not run. `Request extends LazyReadable`, and
|
|
169
|
+
* LazyReadable's prototype is Readable's, which keeps `req instanceof Readable` true and every
|
|
170
|
+
* Readable method reachable; what is missing is `_readableState`, and that is built on the first
|
|
171
|
+
* touch. A derived class cannot skip its super() call, but a base class with nothing in it costs
|
|
172
|
+
* nothing to call.
|
|
173
|
+
*
|
|
174
|
+
* The wrapping below is generated rather than written out, and deliberately: every own member of
|
|
175
|
+
* Readable's prototype gets a version that materialises first, so there is no list to keep in step
|
|
176
|
+
* and no door left unguarded. Missing one would not be a slow path, it would be a TypeError on
|
|
177
|
+
* `undefined._readableState` in whatever corner of a stream nobody tested.
|
|
178
|
+
*/
|
|
179
|
+
class LazyReadableBase {}
|
|
180
|
+
Object.setPrototypeOf(LazyReadableBase.prototype, Readable.prototype);
|
|
181
|
+
Object.setPrototypeOf(LazyReadableBase, Readable);
|
|
182
|
+
|
|
183
|
+
// what the chain says at runtime, said again for the type checker, which cannot see a prototype
|
|
184
|
+
// being reassigned: everything a Readable offers is reachable from a Request, and is a Readable's
|
|
185
|
+
const LazyReadable = /** @type {typeof Readable} */ (/** @type {unknown} */ (LazyReadableBase));
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Builds the stream this object has been pretending to be. Idempotent: everything that can be
|
|
189
|
+
* reached from outside goes through it, so it is called far more often than it does anything.
|
|
190
|
+
*
|
|
191
|
+
* EventEmitter's init keeps an _events that is already there, so listeners added before this
|
|
192
|
+
* survive it.
|
|
193
|
+
*
|
|
194
|
+
* @param {any} stream
|
|
195
|
+
*/
|
|
196
|
+
function materialise(stream) {
|
|
197
|
+
if (stream._readableState === undefined) {
|
|
198
|
+
Readable.call(stream, READABLE_OPTIONS);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const member of [
|
|
203
|
+
...Object.getOwnPropertyNames(Readable.prototype),
|
|
204
|
+
...Object.getOwnPropertySymbols(Readable.prototype)
|
|
205
|
+
]) {
|
|
206
|
+
// the constructor is not a door, and `readable` is handled below because a request writes it
|
|
207
|
+
// and writing it must not build the very thing this is avoiding
|
|
208
|
+
if (member === "constructor" || member === "readable") {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Readable.prototype, member));
|
|
212
|
+
if (typeof descriptor.value === "function") {
|
|
213
|
+
const inner = descriptor.value;
|
|
214
|
+
Object.defineProperty(LazyReadableBase.prototype, member, {
|
|
215
|
+
...descriptor,
|
|
216
|
+
/** @this {any} @param {...any} args */
|
|
217
|
+
value: function (...args) {
|
|
218
|
+
materialise(this);
|
|
219
|
+
return inner.apply(this, args);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
} else if (descriptor.get || descriptor.set) {
|
|
223
|
+
const innerGet = descriptor.get;
|
|
224
|
+
const innerSet = descriptor.set;
|
|
225
|
+
Object.defineProperty(LazyReadableBase.prototype, member, {
|
|
226
|
+
...descriptor,
|
|
227
|
+
get: innerGet
|
|
228
|
+
? /** @this {any} */ function () {
|
|
229
|
+
materialise(this);
|
|
230
|
+
return innerGet.call(this);
|
|
231
|
+
}
|
|
232
|
+
: undefined,
|
|
233
|
+
set: innerSet
|
|
234
|
+
? /** @this {any} @param {any} value */ function (value) {
|
|
235
|
+
materialise(this);
|
|
236
|
+
innerSet.call(this, value);
|
|
237
|
+
}
|
|
238
|
+
: undefined
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const nodeReadable = /** @type {PropertyDescriptor} */ (
|
|
244
|
+
Object.getOwnPropertyDescriptor(Readable.prototype, "readable")
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
// `readable` on its own: a request sets it while it is being built, and node's setter is a no-op
|
|
248
|
+
// without the state anyway, so the flag is kept as a plain field until there is a stream to ask
|
|
249
|
+
Object.defineProperty(LazyReadableBase.prototype, "readable", {
|
|
250
|
+
configurable: true,
|
|
251
|
+
enumerable: false,
|
|
252
|
+
/** @this {any} */
|
|
253
|
+
get: function () {
|
|
254
|
+
return this._readableState === undefined
|
|
255
|
+
? this._readableFlag === true
|
|
256
|
+
: /** @type {any} */ (nodeReadable.get).call(this);
|
|
257
|
+
},
|
|
258
|
+
/** @this {any} @param {any} value */
|
|
259
|
+
set: function (value) {
|
|
260
|
+
if (this._readableState === undefined) {
|
|
261
|
+
this._readableFlag = !!value;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
/** @type {any} */ (nodeReadable.set).call(this, value);
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
module.exports = class Request extends LazyReadable {
|
|
133
269
|
/** @type {Record<string, any>|null} */
|
|
134
270
|
#cachedQuery = null;
|
|
135
271
|
|
|
@@ -200,13 +336,20 @@ module.exports = class Request extends Readable {
|
|
|
200
336
|
) {
|
|
201
337
|
r._connectionClose = true;
|
|
202
338
|
} else if (
|
|
203
|
-
|
|
204
|
-
// nothing: the stream ends empty either way, without the onData subscription
|
|
205
|
-
(headerKey.length === 14 && headerKey === "content-length" && value !== "0") ||
|
|
339
|
+
(headerKey.length === 14 && headerKey === "content-length") ||
|
|
206
340
|
(headerKey.length === 17 && headerKey === "transfer-encoding")
|
|
207
341
|
) {
|
|
208
|
-
//
|
|
209
|
-
|
|
342
|
+
// saying anything about framing at all, "0" included. A parser that can see a
|
|
343
|
+
// content-length answers about the body it describes, even an empty one: a zero length
|
|
344
|
+
// with a charset nobody can decode is a 415 in express and here, so a chain may only
|
|
345
|
+
// step over a parser when the request said nothing about a body whatsoever
|
|
346
|
+
r._hasBodyHeaders = true;
|
|
347
|
+
// content-length: 0 declares that there is nothing, which is the same as declaring
|
|
348
|
+
// nothing: the stream ends empty either way, without the onData subscription
|
|
349
|
+
if (value !== "0" || headerKey.length === 17) {
|
|
350
|
+
// noticed here so the body decision in the constructor does not build the headers object
|
|
351
|
+
r._declaresBody = true;
|
|
352
|
+
}
|
|
210
353
|
}
|
|
211
354
|
};
|
|
212
355
|
|
|
@@ -216,6 +359,95 @@ module.exports = class Request extends Readable {
|
|
|
216
359
|
*/
|
|
217
360
|
optimizedParams;
|
|
218
361
|
|
|
362
|
+
/**
|
|
363
|
+
* Whether a body parser has already read this request, so a second one leaves it alone.
|
|
364
|
+
* @type {boolean|undefined}
|
|
365
|
+
*/
|
|
366
|
+
bodyRead;
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* The route currently running, which express hands to a handler through the request.
|
|
370
|
+
* @type {any}
|
|
371
|
+
*/
|
|
372
|
+
route;
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Which hop the error being carried came from, so an error handler declared before it does
|
|
376
|
+
* not catch what happened after it.
|
|
377
|
+
* @type {number|undefined}
|
|
378
|
+
*/
|
|
379
|
+
_errorKey;
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Which app.route() the failing route belonged to, when it belonged to one. Express builds one
|
|
383
|
+
* route out of everything hung off an app.route(), so an error handler written on it catches
|
|
384
|
+
* what its siblings raised, and nothing else does.
|
|
385
|
+
* @type {number|undefined}
|
|
386
|
+
*/
|
|
387
|
+
_errorGroup;
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* How much of _originalPath the mounts entered so far have taken. Kept as a count rather than
|
|
391
|
+
* worked out from the mount patterns, because what a mount took is what it matched, and a
|
|
392
|
+
* pattern rebuilt from the whole stack does not always match the same thing.
|
|
393
|
+
* @type {number}
|
|
394
|
+
*/
|
|
395
|
+
_consumed = 0;
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* next() as the router means it: the rest of the route is skipped. res.sendFile reports its
|
|
399
|
+
* failures here, because express reports them to the router and not to the route.
|
|
400
|
+
* @type {((err?: any) => void)|undefined}
|
|
401
|
+
*/
|
|
402
|
+
_leaveRoute;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* What `readable` answers while there is no stream to ask, see LazyReadable. Declared so the
|
|
406
|
+
* class has one shape whether or not anything ever streams.
|
|
407
|
+
* @type {boolean}
|
|
408
|
+
*/
|
|
409
|
+
_readableFlag = true;
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* The peer address as uWS hands it over, sixteen bytes or four.
|
|
413
|
+
*
|
|
414
|
+
* Declared although the constructor only sometimes fills it in: a property that appears on
|
|
415
|
+
* some requests and not others gives the class more than one shape, and every read of every
|
|
416
|
+
* other field pays for that.
|
|
417
|
+
*
|
|
418
|
+
* @type {ArrayBuffer|undefined}
|
|
419
|
+
*/
|
|
420
|
+
rawIp;
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Whether rawIp came from a PROXY protocol preamble rather than from the socket. Only the
|
|
424
|
+
* IPv4 mapping reads it, see parsedIp. Declared for the same reason as rawIp.
|
|
425
|
+
* @type {boolean}
|
|
426
|
+
*/
|
|
427
|
+
_ipFromProxy = false;
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Whether the request declared a body, content-length or transfer-encoding, spotted during
|
|
431
|
+
* the header copy. Declared for the same reason as rawIp.
|
|
432
|
+
* @type {boolean|undefined}
|
|
433
|
+
*/
|
|
434
|
+
_declaresBody;
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Whether the request said anything at all about framing, a content-length of "0" included.
|
|
438
|
+
* Wider than _declaresBody on purpose: a parser that can see a content-length answers about
|
|
439
|
+
* the body it describes even when that body is empty, so this is what decides whether a chain
|
|
440
|
+
* may step over one. Declared for the same reason as rawIp.
|
|
441
|
+
* @type {boolean|undefined}
|
|
442
|
+
*/
|
|
443
|
+
_hasBodyHeaders;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
447
|
+
* @type {boolean|undefined}
|
|
448
|
+
*/
|
|
449
|
+
_connectionClose;
|
|
450
|
+
|
|
219
451
|
/**
|
|
220
452
|
* The continuation of the chain currently running, which express also hands to a handler
|
|
221
453
|
* through the request. Declared rather than left to appear on assignment: runRoute sets it
|
|
@@ -252,19 +484,32 @@ module.exports = class Request extends Readable {
|
|
|
252
484
|
* literal registration, a holder of its own for a parameterised one
|
|
253
485
|
*/
|
|
254
486
|
constructor(req, res, app, preset, skipHolder) {
|
|
255
|
-
// the
|
|
256
|
-
super(
|
|
487
|
+
// nothing: the stream is built on the first touch, see LazyReadable
|
|
488
|
+
super();
|
|
257
489
|
this._res = res;
|
|
258
490
|
this._req = req;
|
|
259
|
-
|
|
491
|
+
// the plain field behind the `readable` accessor, written rather than set so a request that
|
|
492
|
+
// never streams never builds a stream
|
|
493
|
+
this._readableFlag = true;
|
|
260
494
|
if (skipHolder !== undefined && skipHolder.skipHeaders) {
|
|
261
495
|
// The chain behind this registration provably never reads a header, so instead of
|
|
262
496
|
// copying them all out of uWS the constructor asks for the four that steer the
|
|
263
497
|
// framework itself: body framing, keep-alive, and accept for the error page a
|
|
264
498
|
// throw could still need. A GET that does declare a body is the rare case, and
|
|
265
499
|
// the parsers and the stream want the whole picture, so it takes the full copy.
|
|
500
|
+
//
|
|
501
|
+
// Seven named reads against one forEach looks like it should lose, and does not: the
|
|
502
|
+
// seven are flat at 0.75us however many headers are on the wire, since each one is a
|
|
503
|
+
// napi crossing and the scan behind it is nothing, while the copy pays a hop back into
|
|
504
|
+
// JS per header and grows, 1.16us at four headers, 1.61 at eight, 2.90 at sixteen. They
|
|
505
|
+
// do not cross, and the gap widens exactly where real traffic lives, since a browser
|
|
506
|
+
// sends a dozen or more. The body case pays two of the seven and then copies anyway,
|
|
507
|
+
// which is 0.2us on a request that is about to read a body.
|
|
266
508
|
const length = req.getHeader("content-length");
|
|
267
509
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
510
|
+
if (length !== "" || transferEncoding !== "") {
|
|
511
|
+
this._hasBodyHeaders = true;
|
|
512
|
+
}
|
|
268
513
|
if ((length !== "" && length !== "0") || transferEncoding !== "") {
|
|
269
514
|
currentRequest = this;
|
|
270
515
|
this._req.forEach(Request.#collectHeader);
|
|
@@ -304,10 +549,6 @@ module.exports = class Request extends Readable {
|
|
|
304
549
|
currentRequest = null;
|
|
305
550
|
}
|
|
306
551
|
this.routeCount = 1;
|
|
307
|
-
this.key = key++;
|
|
308
|
-
if (key > 100000) {
|
|
309
|
-
key = 0;
|
|
310
|
-
}
|
|
311
552
|
this.app = app;
|
|
312
553
|
// both forms are kept, because both are asked for: the query with its "?" goes into
|
|
313
554
|
// req.url, and req.query parses the raw one. Keeping only the first meant slicing the "?"
|
|
@@ -318,8 +559,11 @@ module.exports = class Request extends Readable {
|
|
|
318
559
|
this._rawQuery = "";
|
|
319
560
|
this.urlQuery = "";
|
|
320
561
|
} else {
|
|
321
|
-
|
|
322
|
-
|
|
562
|
+
// getQuery tells "/a" from "/a?": no query string at all reads undefined, an empty
|
|
563
|
+
// one reads "". Express keeps that lone "?" in req.url, so the two are kept apart
|
|
564
|
+
const rawQuery = req.getQuery();
|
|
565
|
+
this._rawQuery = rawQuery ?? "";
|
|
566
|
+
this.urlQuery = rawQuery === undefined ? "" : "?" + rawQuery;
|
|
323
567
|
}
|
|
324
568
|
if (preset) {
|
|
325
569
|
// the registration's constants: two native crossings and their strings not asked for
|
|
@@ -347,9 +591,6 @@ module.exports = class Request extends Readable {
|
|
|
347
591
|
this.endsWithSlash = this.path.charCodeAt(this.path.length - 1) === 0x2f;
|
|
348
592
|
this._opPath = this.path;
|
|
349
593
|
this._originalPath = this.path;
|
|
350
|
-
if (this.endsWithSlash && this.path !== "/" && !this.app.get("strict routing")) {
|
|
351
|
-
this._opPath = this._opPath.slice(0, -1);
|
|
352
|
-
}
|
|
353
594
|
this.method = req.getCaseSensitiveMethod().toUpperCase();
|
|
354
595
|
this._isOptions = this.method === "OPTIONS";
|
|
355
596
|
this._isHead = this.method === "HEAD";
|
|
@@ -370,16 +611,24 @@ module.exports = class Request extends Readable {
|
|
|
370
611
|
// null for the same reason as the two above: a request that never enters a mount never
|
|
371
612
|
// needs either array, and the push sites materialize them
|
|
372
613
|
this._stack = null;
|
|
373
|
-
//
|
|
374
|
-
//
|
|
375
|
-
this.
|
|
614
|
+
// how many characters of _originalPath the mounts entered so far have taken, which is
|
|
615
|
+
// where baseUrl ends and the path below them begins
|
|
616
|
+
this._consumed = 0;
|
|
376
617
|
this._paramStack = null;
|
|
618
|
+
// route and application in pairs, one pair per mounted application entered from another
|
|
619
|
+
// application, so handing back puts the one that was current back, see rememberApp
|
|
620
|
+
this._appStack = undefined;
|
|
377
621
|
this.receivedData = false;
|
|
378
622
|
// reading ip is very slow in UWS, so its better to not do it unless truly needed
|
|
379
|
-
if (
|
|
380
|
-
//
|
|
381
|
-
//
|
|
382
|
-
this.rawIp = this.
|
|
623
|
+
if (app.needsIpAfterResponse) {
|
|
624
|
+
// an app that has been seen asking after the response reads it now, because by then
|
|
625
|
+
// µWS has freed it
|
|
626
|
+
this.rawIp = this._readRawIp();
|
|
627
|
+
} else if (app._ipProbes < 100) {
|
|
628
|
+
// and until this app has been seen either way, the first hundred requests read it, so
|
|
629
|
+
// one of them can be the one that finds out
|
|
630
|
+
app._ipProbes++;
|
|
631
|
+
this.rawIp = this._readRawIp();
|
|
383
632
|
}
|
|
384
633
|
|
|
385
634
|
// A body exists on the wire only when the request declares one, content-length or
|
|
@@ -483,8 +732,8 @@ module.exports = class Request extends Readable {
|
|
|
483
732
|
if (this._baseUrlOverride !== undefined) {
|
|
484
733
|
return this._baseUrlOverride;
|
|
485
734
|
}
|
|
486
|
-
|
|
487
|
-
return
|
|
735
|
+
// what the mounts took, which is where the path they left off begins
|
|
736
|
+
return this._consumed === 0 ? "" : this._originalPath.slice(0, this._consumed);
|
|
488
737
|
}
|
|
489
738
|
|
|
490
739
|
/**
|
|
@@ -641,38 +890,50 @@ module.exports = class Request extends Readable {
|
|
|
641
890
|
? this._originalPath
|
|
642
891
|
: this._originalPath.slice(0, this._originalPath.length - oldPath.length);
|
|
643
892
|
this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
|
|
644
|
-
|
|
893
|
+
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
894
|
+
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
645
895
|
this.#cachedQuery = null;
|
|
646
896
|
this._originalPath = prefix + newPath;
|
|
647
897
|
this.path = newPath;
|
|
648
898
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
649
|
-
this._opPath =
|
|
650
|
-
this.endsWithSlash && newPath !== "/" && !this.app.get("strict routing") ? newPath.slice(0, -1) : newPath;
|
|
899
|
+
this._opPath = newPath;
|
|
651
900
|
this._lastUrl = newUrl;
|
|
652
901
|
}
|
|
653
902
|
|
|
654
903
|
/**
|
|
655
|
-
* The query string parsed by whichever parser the "query parser" setting names
|
|
656
|
-
*
|
|
657
|
-
*
|
|
904
|
+
* The query string parsed by whichever parser the "query parser" setting names. A null-prototype
|
|
905
|
+
* object, so a key like "__proto__" cannot reach Object.prototype. No setter, so assigning to
|
|
906
|
+
* req.query throws as it does on Express.
|
|
907
|
+
*
|
|
908
|
+
* Every read answers a new object, because express's getter re-parses on every read and so hands
|
|
909
|
+
* one back too. Two consequences an application can see, and both of them bite: `req.query` is
|
|
910
|
+
* never the object another reader holds, and a write to a key of it is gone by the next read.
|
|
911
|
+
* That second one is how express-validator's sanitisers behave: `.trim()` on a query parameter
|
|
912
|
+
* changes nothing an ordinary handler will see, which is why it also offers matchedData(). With
|
|
913
|
+
* the parse cached and handed out as itself, the sanitised value leaked into req.query here and
|
|
914
|
+
* a handler written against express read a trimmed value where express gives it the raw one.
|
|
915
|
+
*
|
|
916
|
+
* The parse itself is still done once. What is copied per read is the shallow result, which is
|
|
917
|
+
* cheaper than express's re-parse and answers the same for everything but a write to a nested
|
|
918
|
+
* key, which only the extended parser can produce.
|
|
658
919
|
*
|
|
659
920
|
* @returns {Record<string, any>}
|
|
660
921
|
*/
|
|
661
922
|
get query() {
|
|
662
|
-
|
|
663
|
-
|
|
923
|
+
let parsed = this.#cachedQuery;
|
|
924
|
+
if (parsed === null) {
|
|
925
|
+
const qp = this.app.get("query parser fn");
|
|
926
|
+
// the vendored default already answers on a bare null prototype, so it goes out as is;
|
|
927
|
+
// any other parser is copied onto one, which is what kept fast-querystring's result from
|
|
928
|
+
// inspecting as "Empty <[Object: null prototype] {}>" where Express shows the bare form
|
|
929
|
+
parsed = qp
|
|
930
|
+
? qp === parseQuery
|
|
931
|
+
? parseQuery(this._rawQuery)
|
|
932
|
+
: Object.assign(Object.create(null), qp(this._rawQuery))
|
|
933
|
+
: Object.create(null);
|
|
934
|
+
this.#cachedQuery = parsed;
|
|
664
935
|
}
|
|
665
|
-
|
|
666
|
-
// the vendored default already answers on a bare null prototype, so it goes out as is;
|
|
667
|
-
// any other parser is copied onto one, which is what kept fast-querystring's result from
|
|
668
|
-
// inspecting as "Empty <[Object: null prototype] {}>" where Express shows the bare form
|
|
669
|
-
const parsed = qp
|
|
670
|
-
? qp === parseQuery
|
|
671
|
-
? parseQuery(this._rawQuery)
|
|
672
|
-
: Object.assign(Object.create(null), qp(this._rawQuery))
|
|
673
|
-
: Object.create(null);
|
|
674
|
-
this.#cachedQuery = parsed;
|
|
675
|
-
return parsed;
|
|
936
|
+
return Object.assign(Object.create(null), parsed);
|
|
676
937
|
}
|
|
677
938
|
|
|
678
939
|
/**
|
|
@@ -717,6 +978,32 @@ module.exports = class Request extends Readable {
|
|
|
717
978
|
return typeof val === "string" && val.toLowerCase() === "xmlhttprequest";
|
|
718
979
|
}
|
|
719
980
|
|
|
981
|
+
/**
|
|
982
|
+
* The peer address bytes, from the socket or, when the application asked for it, from a PROXY
|
|
983
|
+
* protocol preamble the load balancer in front of this server sent ahead of the request.
|
|
984
|
+
*
|
|
985
|
+
* The setting is off by default and has to stay that way. µWS parses the preamble from whoever
|
|
986
|
+
* sends it, with nothing to ask for it at listen time and no way to restrict who may, so an
|
|
987
|
+
* application that took the address unconditionally would let any client claim any address:
|
|
988
|
+
* the first sixteen bytes of a connection are enough to become 10.0.0.1 for a rate limiter, an
|
|
989
|
+
* allow list or an audit log. Turn it on only when nothing can reach this server except the
|
|
990
|
+
* proxy in front of it.
|
|
991
|
+
*
|
|
992
|
+
* @returns {ArrayBuffer} the socket's own address when no preamble arrived
|
|
993
|
+
*/
|
|
994
|
+
_readRawIp() {
|
|
995
|
+
const uwsRes = this._res;
|
|
996
|
+
if (this.app.get("trust proxy protocol")) {
|
|
997
|
+
const proxied = uwsRes.getProxiedRemoteAddress();
|
|
998
|
+
// empty unless a preamble arrived, which is the only thing that tells the two apart
|
|
999
|
+
if (proxied.byteLength !== 0) {
|
|
1000
|
+
this._ipFromProxy = true;
|
|
1001
|
+
return proxied;
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return uwsRes.getRemoteAddress();
|
|
1005
|
+
}
|
|
1006
|
+
|
|
720
1007
|
/**
|
|
721
1008
|
* The peer address as text, read from uWS and cached. Reading it is expensive and it is gone
|
|
722
1009
|
* once the response has finished, so it is read up front for the first hundred requests, and
|
|
@@ -739,24 +1026,39 @@ module.exports = class Request extends Readable {
|
|
|
739
1026
|
// fallback once
|
|
740
1027
|
return mapsIPv4Peer(this.app) ? "::ffff:127.0.0.1" : "127.0.0.1";
|
|
741
1028
|
}
|
|
742
|
-
this.rawIp = this.
|
|
1029
|
+
this.rawIp = this._readRawIp();
|
|
743
1030
|
}
|
|
1031
|
+
// read once: the branch above settled it, and every use below wants the bytes
|
|
1032
|
+
const rawIp = /** @type {ArrayBuffer} */ (this.rawIp);
|
|
744
1033
|
/** @type {string|undefined} */
|
|
745
1034
|
let ip;
|
|
746
|
-
if (
|
|
1035
|
+
if (rawIp.byteLength === 4) {
|
|
747
1036
|
// ipv4
|
|
748
|
-
ip = new Uint8Array(
|
|
749
|
-
|
|
1037
|
+
ip = new Uint8Array(rawIp).join(".");
|
|
1038
|
+
// the mapped form belongs to a dual stack listener, which is what makes an IPv4 peer
|
|
1039
|
+
// arrive as ::ffff:a.b.c.d. An address a proxy declared never came through that socket,
|
|
1040
|
+
// so it is left as the four numbers the proxy sent
|
|
1041
|
+
if (!this._ipFromProxy && mapsIPv4Peer(this.app)) {
|
|
750
1042
|
ip = "::ffff:" + ip;
|
|
751
1043
|
}
|
|
752
|
-
} else if (
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
groups
|
|
1044
|
+
} else if (rawIp.byteLength === 16) {
|
|
1045
|
+
const bytes = new Uint8Array(rawIp);
|
|
1046
|
+
if (isMappedIPv4(bytes)) {
|
|
1047
|
+
// ::ffff:a.b.c.d, which is what a dual stack listener hands over for every IPv4
|
|
1048
|
+
// peer, so it is what nearly every request here is. The general path below reaches
|
|
1049
|
+
// the same string through a DataView, an array of eight groups and a scan for the
|
|
1050
|
+
// longest run of zeros, and measured 157ns more per request for it. Anything that
|
|
1051
|
+
// reads req.ip pays that once, and morgan reads it on every line it writes.
|
|
1052
|
+
ip = "::ffff:" + bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
|
|
1053
|
+
} else {
|
|
1054
|
+
// ipv6
|
|
1055
|
+
const dv = new DataView(rawIp);
|
|
1056
|
+
const groups = new Array(8);
|
|
1057
|
+
for (let i = 0; i < 8; i++) {
|
|
1058
|
+
groups[i] = dv.getUint16(i * 2);
|
|
1059
|
+
}
|
|
1060
|
+
ip = formatIPv6(groups);
|
|
758
1061
|
}
|
|
759
|
-
ip = formatIPv6(groups);
|
|
760
1062
|
} else {
|
|
761
1063
|
ip = undefined; // unix sockets dont have ip
|
|
762
1064
|
}
|
|
@@ -809,12 +1111,15 @@ module.exports = class Request extends Readable {
|
|
|
809
1111
|
_detachFromResponse() {
|
|
810
1112
|
const uwsRes = this._res;
|
|
811
1113
|
if (!this.rawIp) {
|
|
812
|
-
this.rawIp =
|
|
1114
|
+
this.rawIp = this._readRawIp();
|
|
813
1115
|
}
|
|
814
1116
|
const remotePort = uwsRes.getRemotePort();
|
|
815
1117
|
const rawIp = this.rawIp;
|
|
816
1118
|
this._res = {
|
|
817
1119
|
getRemoteAddress: () => rawIp,
|
|
1120
|
+
// whatever a preamble said is already in rawIp, and asking again is the use after free
|
|
1121
|
+
// this method exists to avoid
|
|
1122
|
+
getProxiedRemoteAddress: () => emptyAddress,
|
|
818
1123
|
getRemotePort: () => remotePort,
|
|
819
1124
|
// a body cannot arrive on an upgraded socket, and a stray reader must not reach µWS
|
|
820
1125
|
onData() {},
|