fulmine.js 5.0.0 → 5.1.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/src/request.js CHANGED
@@ -83,6 +83,20 @@ function formatIPv6(groups) {
83
83
  return out;
84
84
  }
85
85
 
86
+ /**
87
+ * Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps
88
+ * it whenever the listener is dual stack, which is every listen() not given an IPv4 address to
89
+ * bind. uWS already hands mapped peers over as sixteen bytes; four bytes only reach req.ip from a
90
+ * v4-bound native listener or through the node shim, whose server supertest binds dual stack.
91
+ *
92
+ * @param {any} app the application the request arrived at
93
+ * @returns {boolean}
94
+ */
95
+ function mapsIPv4Peer(app) {
96
+ const host = app._listenHost;
97
+ return !(host && isIP(host) === 4);
98
+ }
99
+
86
100
  const discardedDuplicates = new Set([
87
101
  "age",
88
102
  "authorization",
@@ -109,6 +123,11 @@ let key = 0;
109
123
  // 128 KB of body buffered before uWS is asked to pause
110
124
  const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
111
125
 
126
+ // Whose headers the shared collector below is filling. uWS's forEach is synchronous and runs no
127
+ // user code, so the handoff cannot interleave; module-level so the callback exists once instead
128
+ // of once per request.
129
+ let currentRequest = null;
130
+
112
131
  module.exports = class Request extends Readable {
113
132
  /** @type {Record<string, any>|null} */
114
133
  #cachedQuery = null;
@@ -121,7 +140,8 @@ module.exports = class Request extends Readable {
121
140
 
122
141
  // Flat, name then value: an array of pairs meant one array allocated per header on every
123
142
  // request, and a request carries eight or ten of them. Everything that reads this walks it two
124
- // at a time.
143
+ // at a time. The names are lowercase by contract: uWS lowers them on the wire and the node
144
+ // shim lowers them in its forEach, so readers compare without lowering again.
125
145
  #rawHeadersEntries = [];
126
146
 
127
147
  /** @type {string|undefined|null} */
@@ -133,6 +153,30 @@ module.exports = class Request extends Readable {
133
153
 
134
154
  res;
135
155
 
156
+ // one function for every request, fed through currentRequest: an arrow in the constructor
157
+ // captured `this`, which cost a context and a function allocation per request
158
+ static #collectHeader = (headerKey, value) => {
159
+ const r = currentRequest;
160
+ r.#rawHeadersEntries.push(headerKey, value);
161
+ // spotted in the loop that is running anyway: a client asking for the connection to be
162
+ // closed must not be answered that it is being kept alive. The response is built right
163
+ // after this and reads the flag.
164
+ if (
165
+ headerKey.length === 10 &&
166
+ headerKey === "connection" &&
167
+ value.length === 5 &&
168
+ value.toLowerCase() === "close"
169
+ ) {
170
+ r._connectionClose = true;
171
+ } else if (
172
+ (headerKey.length === 14 && headerKey === "content-length") ||
173
+ (headerKey.length === 17 && headerKey === "transfer-encoding")
174
+ ) {
175
+ // noticed here so the body decision in the constructor does not build the headers object
176
+ r._declaresBody = true;
177
+ }
178
+ };
179
+
136
180
  optimizedParams;
137
181
 
138
182
  _error;
@@ -154,15 +198,9 @@ module.exports = class Request extends Readable {
154
198
  this._res = res;
155
199
  this._req = req;
156
200
  this.readable = true;
157
- this._req.forEach((key, value) => {
158
- this.#rawHeadersEntries.push(key, value);
159
- // spotted in the loop that is running anyway: a client asking for the connection to be
160
- // closed must not be answered that it is being kept alive. The response is built right
161
- // after this and reads the flag.
162
- if (key.length === 10 && key === "connection" && value.length === 5 && value.toLowerCase() === "close") {
163
- this._connectionClose = true;
164
- }
165
- });
201
+ currentRequest = this;
202
+ this._req.forEach(Request.#collectHeader);
203
+ currentRequest = null;
166
204
  this.routeCount = 1;
167
205
  this.key = key++;
168
206
  if (key > 100000) {
@@ -206,11 +244,13 @@ module.exports = class Request extends Readable {
206
244
  // The router builds it the first time it has something to put in it.
207
245
  this._matchedMethods = this._isOptions ? new Set() : null;
208
246
  this._paramCalled = null;
209
- this._stack = [];
247
+ // null for the same reason as the two above: a request that never enters a mount never
248
+ // needs either array, and the push sites materialize them
249
+ this._stack = null;
210
250
  // number of entries in _stack that aren't the empty path. while this is 0 the whole
211
251
  // stack joins to "", so getFullMountpath can skip the join entirely
212
252
  this._stackMounted = 0;
213
- this._paramStack = [];
253
+ this._paramStack = null;
214
254
  this.receivedData = false;
215
255
  // reading ip is very slow in UWS, so its better to not do it unless truly needed
216
256
  if (this.app.needsIpAfterResponse || this.key < 100) {
@@ -227,32 +267,46 @@ module.exports = class Request extends Readable {
227
267
  this.method === "PUT" ||
228
268
  this.method === "PATCH" ||
229
269
  this.method === "QUERY" ||
230
- (additionalMethods && additionalMethods.includes(this.method))
270
+ (additionalMethods && additionalMethods.includes(this.method)) ||
271
+ // any request that declares a body carries one, whatever the verb: a GET with
272
+ // content-length left unread would end this stream empty and poison the keep-alive
273
+ // connection with its unconsumed bytes. uWS itself discards GET bodies, so this is
274
+ // the node shim's path
275
+ /** @type {any} */ (this)._declaresBody
231
276
  ) {
232
- this._res.onData((ab, isLast) => {
233
- this.receivedData = true;
234
- if (this.#responseEnded) {
235
- return;
236
- }
237
- // ab.slice(0) copies the ArrayBuffer; uWS neuters `ab` after this callback,
238
- // so a Buffer.from(ab) view would corrupt data left in the Readable queue.
239
- const chunk = Buffer.from(ab.slice(0));
240
- const accepted = this.push(chunk);
241
- // push() may synchronously end the response via a flowing-mode listener.
242
- if (!accepted && !isLast && !this.#responseEnded) {
243
- this._res.pause();
244
- this.#paused = true;
245
- }
246
- if (isLast) {
247
- this.push(null);
248
- }
249
- });
277
+ this._subscribeBody();
250
278
  } else {
251
279
  this.receivedData = true;
252
280
  this.push(null);
253
281
  }
254
282
  }
255
283
 
284
+ /**
285
+ * Subscribes to the uWS body stream. Out of the constructor so a bodyless request allocates
286
+ * no closure at all there; must still run during the constructor call, since uWS only feeds a
287
+ * handler registered before the route handler returns.
288
+ */
289
+ _subscribeBody() {
290
+ this._res.onData((ab, isLast) => {
291
+ this.receivedData = true;
292
+ if (this.#responseEnded) {
293
+ return;
294
+ }
295
+ // ab.slice(0) copies the ArrayBuffer; uWS neuters `ab` after this callback,
296
+ // so a Buffer.from(ab) view would corrupt data left in the Readable queue.
297
+ const chunk = Buffer.from(ab.slice(0));
298
+ const accepted = this.push(chunk);
299
+ // push() may synchronously end the response via a flowing-mode listener.
300
+ if (!accepted && !isLast && !this.#responseEnded) {
301
+ this._res.pause();
302
+ this.#paused = true;
303
+ }
304
+ if (isLast) {
305
+ this.push(null);
306
+ }
307
+ });
308
+ }
309
+
256
310
  /**
257
311
  * Whether there is any point still reading the body: once the response is finished or the
258
312
  * connection is gone, uWS has nothing left to hand over.
@@ -405,7 +459,10 @@ module.exports = class Request extends Readable {
405
459
  * @returns {string}
406
460
  */
407
461
  get protocol() {
408
- const proto = this.app.ssl ? "https" : "http";
462
+ // express reads socket.encrypted, and middleware does assign to the stand-in; the app's
463
+ // own ssl flag answers when nothing has built the stand-in yet
464
+ const conn = this.#cachedConnection;
465
+ const proto = (conn ? conn.encrypted : this.app.ssl) ? "https" : "http";
409
466
  const trust = this.app.get("trust proxy fn");
410
467
  if (!trust) {
411
468
  return proto;
@@ -481,7 +538,7 @@ module.exports = class Request extends Readable {
481
538
 
482
539
  /**
483
540
  * The hostname's subdomains, furthest from the root first, dropping the last
484
- * "subdomain offset" labels. Empty for an IP address.
541
+ * "subdomain offset" labels. An IP host is one label, never split on its dots.
485
542
  * @returns {string[]}
486
543
  */
487
544
  get subdomains() {
@@ -490,15 +547,14 @@ module.exports = class Request extends Readable {
490
547
  }
491
548
 
492
549
  const hostname = this.hostname;
493
- if (!hostname || isIP(hostname)) {
550
+ if (!hostname) {
494
551
  return (this.#cachedSubdomains = []);
495
552
  }
496
553
 
497
554
  const offset = this.app.get("subdomain offset");
498
- const parts = hostname.split(".");
499
- const subdomains = parts.reverse().slice(offset);
555
+ const parts = isIP(hostname) ? [hostname] : hostname.split(".").reverse();
500
556
 
501
- return (this.#cachedSubdomains = subdomains);
557
+ return (this.#cachedSubdomains = parts.slice(offset));
502
558
  }
503
559
 
504
560
  /**
@@ -531,7 +587,7 @@ module.exports = class Request extends Readable {
531
587
  if (!this.rawIp) {
532
588
  if (finished) {
533
589
  // fallback once
534
- return "127.0.0.1";
590
+ return mapsIPv4Peer(this.app) ? "::ffff:127.0.0.1" : "127.0.0.1";
535
591
  }
536
592
  this.rawIp = this._res.getRemoteAddress();
537
593
  }
@@ -540,6 +596,9 @@ module.exports = class Request extends Readable {
540
596
  if (this.rawIp.byteLength === 4) {
541
597
  // ipv4
542
598
  ip = new Uint8Array(this.rawIp).join(".");
599
+ if (mapsIPv4Peer(this.app)) {
600
+ ip = "::ffff:" + ip;
601
+ }
543
602
  } else if (this.rawIp.byteLength === 16) {
544
603
  // ipv6
545
604
  const dv = new DataView(this.rawIp);
@@ -607,10 +666,10 @@ module.exports = class Request extends Readable {
607
666
  const entries = this.#rawHeadersEntries;
608
667
  for (let i = 0, len = entries.length; i < len; i += 2) {
609
668
  const key = entries[i];
610
- // 'if-none-match'.length === 13, 'if-modified-since'.length === 17
669
+ // 'if-none-match'.length === 13, 'if-modified-since'.length === 17; the
670
+ // entries carry lowercase names by contract, so this compares them as they are
611
671
  if (key.length === 13 || key.length === 17) {
612
- const lower = key.toLowerCase();
613
- if (lower === "if-none-match" || lower === "if-modified-since") {
672
+ if (key === "if-none-match" || key === "if-modified-since") {
614
673
  hasConditional = true;
615
674
  break;
616
675
  }
@@ -794,7 +853,8 @@ module.exports = class Request extends Readable {
794
853
  const entries = this.#rawHeadersEntries;
795
854
  for (let index = 0, len = entries.length; index < len; index += 2) {
796
855
  const value = entries[index + 1];
797
- const key = entries[index].toLowerCase();
856
+ // lowercase by the entries' contract, see the field declaration
857
+ const key = entries[index];
798
858
  if (Object.hasOwn(headers, key)) {
799
859
  if (discardedDuplicates.has(key)) {
800
860
  continue;