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/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
- module.exports = class Request extends Readable {
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
- // content-length: 0 declares that there is nothing, which is the same as declaring
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
- // noticed here so the body decision in the constructor does not build the headers object
209
- r._declaresBody = true;
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 same object every time: Readable reads these options and never writes to them
256
- super(READABLE_OPTIONS);
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
- this.readable = true;
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
- this._rawQuery = req.getQuery() ?? "";
322
- this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
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
- // number of entries in _stack that aren't the empty path. while this is 0 the whole
374
- // stack joins to "", so getFullMountpath can skip the join entirely
375
- this._stackMounted = 0;
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 (this.app.needsIpAfterResponse || this.key < 100) {
380
- // if app needs ip after response, read it now because after response its not accessible
381
- // also read it for first 100 requests to not error
382
- this.rawIp = this._res.getRemoteAddress();
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
- const match = this._originalPath.match(this.app.getFullMountpath(this));
487
- return match ? match[0] : "";
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
- this.urlQuery = this._rawQuery === "" ? "" : "?" + this._rawQuery;
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, cached for the
656
- * life of the request. A null-prototype object, so a key like "__proto__" cannot reach
657
- * Object.prototype. No setter, so assigning to req.query throws as it does on Express.
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
- if (this.#cachedQuery) {
663
- return this.#cachedQuery;
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
- const qp = this.app.get("query parser fn");
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._res.getRemoteAddress();
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 (this.rawIp.byteLength === 4) {
1035
+ if (rawIp.byteLength === 4) {
747
1036
  // ipv4
748
- ip = new Uint8Array(this.rawIp).join(".");
749
- if (mapsIPv4Peer(this.app)) {
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 (this.rawIp.byteLength === 16) {
753
- // ipv6
754
- const dv = new DataView(this.rawIp);
755
- const groups = new Array(8);
756
- for (let i = 0; i < 8; i++) {
757
- groups[i] = dv.getUint16(i * 2);
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 = uwsRes.getRemoteAddress();
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() {},