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/README.md +26 -8
- package/package.json +1 -1
- package/src/application.js +114 -20
- package/src/cli.js +3 -3
- package/src/index.js +5 -0
- package/src/middlewares.js +87 -36
- package/src/node-shim.js +2 -1
- package/src/request.js +102 -42
- package/src/response.js +204 -114
- package/src/route.js +180 -0
- package/src/router.js +507 -124
- package/src/utils.js +41 -3
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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;
|