fulmine.js 5.19.2 → 5.19.3
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/package.json +2 -1
- package/src/adopt.js +20 -26
- package/src/application.js +46 -45
- package/src/cli.js +28 -34
- package/src/cluster.js +16 -25
- package/src/compression.js +39 -51
- package/src/declarative.js +586 -538
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +129 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +61 -77
- package/src/nest.js +19 -34
- package/src/node-shim.js +11 -13
- package/src/optimizer.js +598 -0
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +306 -0
- package/src/request.js +101 -513
- package/src/response-utils.js +88 -0
- package/src/response.js +100 -443
- package/src/route.js +4 -5
- package/src/router-utils.js +950 -0
- package/src/router.js +126 -2148
- package/src/server-shape.js +26 -41
- package/src/server-timing.js +16 -29
- package/src/socket.js +208 -0
- package/src/testing.js +39 -42
- package/src/usage.js +16 -21
- package/src/utils.js +49 -59
- package/src/verify.js +18 -28
- package/src/view.js +5 -7
- package/src/walk.js +580 -0
- package/src/websocket.js +19 -20
- package/src/work.js +21 -27
package/src/request.js
CHANGED
|
@@ -22,412 +22,30 @@ const accepts = require("accepts");
|
|
|
22
22
|
const typeis = require("type-is");
|
|
23
23
|
const parseRange = require("range-parser");
|
|
24
24
|
const proxyaddr = require("proxy-addr");
|
|
25
|
-
const { isIP } = require("node:net");
|
|
26
25
|
const fresh = require("fresh");
|
|
27
26
|
const parseQuery = require("./parse-query.js");
|
|
28
|
-
const {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
*/
|
|
45
|
-
function formatIPv6(groups) {
|
|
46
|
-
// longest run of zero groups, leftmost on a tie, which is the run inet_ntop replaces
|
|
47
|
-
let bestStart = -1;
|
|
48
|
-
let bestLength = 0;
|
|
49
|
-
for (let i = 0; i < 8; i++) {
|
|
50
|
-
if (groups[i] !== 0) continue;
|
|
51
|
-
let run = 1;
|
|
52
|
-
while (i + run < 8 && groups[i + run] === 0) run++;
|
|
53
|
-
if (run > bestLength) {
|
|
54
|
-
bestStart = i;
|
|
55
|
-
bestLength = run;
|
|
56
|
-
}
|
|
57
|
-
i += run - 1;
|
|
58
|
-
}
|
|
59
|
-
// a single zero group is written as "0", not as "::"
|
|
60
|
-
if (bestLength < 2) {
|
|
61
|
-
bestStart = -1;
|
|
62
|
-
bestLength = 0;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ::ffff:a.b.c.d, and the deprecated ::a.b.c.d. The test is inet_ntop's own, including that a
|
|
66
|
-
// run of seven leading zeros never reaches it, since group 6 is inside the run by then.
|
|
67
|
-
const mixed =
|
|
68
|
-
bestStart === 0 &&
|
|
69
|
-
(bestLength === 6 || (bestLength === 7 && groups[7] !== 1) || (bestLength === 5 && groups[5] === 0xffff));
|
|
70
|
-
|
|
71
|
-
let out = "";
|
|
72
|
-
for (let i = 0; i < 8; i++) {
|
|
73
|
-
if (bestStart !== -1 && i >= bestStart && i < bestStart + bestLength) {
|
|
74
|
-
if (i === bestStart) out += ":";
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
if (i !== 0) out += ":";
|
|
78
|
-
if (mixed && i === 6) {
|
|
79
|
-
out += `${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
out += groups[i].toString(16);
|
|
83
|
-
}
|
|
84
|
-
// a run reaching the end leaves a trailing group to close the "::"
|
|
85
|
-
if (bestStart !== -1 && bestStart + bestLength === 8) out += ":";
|
|
86
|
-
return out;
|
|
87
|
-
}
|
|
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
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps
|
|
116
|
-
* it whenever the listener is dual stack, which is every listen() not given an IPv4 address to
|
|
117
|
-
* bind. uWS already hands mapped peers over as sixteen bytes; four bytes only reach req.ip from a
|
|
118
|
-
* v4-bound native listener or through the node shim, whose server supertest binds dual stack.
|
|
119
|
-
*
|
|
120
|
-
* @param {any} app the application the request arrived at
|
|
121
|
-
* @returns {boolean}
|
|
122
|
-
*/
|
|
123
|
-
function mapsIPv4Peer(app) {
|
|
124
|
-
const host = app._listenHost;
|
|
125
|
-
return !(host && isIP(host) === 4);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/** What µWS returns for a proxied address when no PROXY protocol preamble arrived. */
|
|
129
|
-
const emptyAddress = new ArrayBuffer(0);
|
|
130
|
-
|
|
131
|
-
const discardedDuplicates = new Set([
|
|
132
|
-
"age",
|
|
133
|
-
"authorization",
|
|
134
|
-
"content-length",
|
|
135
|
-
"content-type",
|
|
136
|
-
"etag",
|
|
137
|
-
"expires",
|
|
138
|
-
"from",
|
|
139
|
-
"host",
|
|
140
|
-
"if-modified-since",
|
|
141
|
-
"if-unmodified-since",
|
|
142
|
-
"last-modified",
|
|
143
|
-
"location",
|
|
144
|
-
"max-forwards",
|
|
145
|
-
"proxy-authorization",
|
|
146
|
-
"referer",
|
|
147
|
-
"retry-after",
|
|
148
|
-
"server",
|
|
149
|
-
"user-agent"
|
|
150
|
-
]);
|
|
151
|
-
|
|
152
|
-
// 128 KB of body buffered before uWS is asked to pause
|
|
153
|
-
const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
154
|
-
|
|
155
|
-
// The methods node's parser accepts, which is the set a request can arrive with behind Express and
|
|
156
|
-
// the set a route can be registered for here. µWS accepts any token, so without this a line like
|
|
157
|
-
// `{"a":1}GET /path HTTP/1.1` is a request to it, with `{"A":1}GET` as the method. See _mustRefuse.
|
|
158
|
-
const KNOWN_METHODS = new Set(require("http").METHODS);
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Whether a request target is bytes node's parser would have accepted, which is printable ASCII
|
|
162
|
-
* and nothing else.
|
|
163
|
-
*
|
|
164
|
-
* µWS takes the target as it finds it and decodes it as UTF-8, so `GET /café` arrives here
|
|
165
|
-
* as a path with an é in it and the overlong encoding of a slash arrives as replacement
|
|
166
|
-
* characters. Node refuses both with a 400 before any application sees them, and it has to: what
|
|
167
|
-
* reaches req.url otherwise is not what is on the wire, and a proxy in front reading the same
|
|
168
|
-
* bytes can disagree with this server about which path was asked for. Control characters are µWS's
|
|
169
|
-
* own to refuse and it does, so the test is one comparison per character rather than two.
|
|
170
|
-
*
|
|
171
|
-
* @param {string} target the path or the query string, as µWS decoded it
|
|
172
|
-
* @returns {boolean}
|
|
173
|
-
*/
|
|
174
|
-
function isAsciiTarget(target) {
|
|
175
|
-
for (let i = 0; i < target.length; i++) {
|
|
176
|
-
if (target.charCodeAt(i) > 0x7e) {
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Whether a transfer-encoding leaves the body's length knowable, which is RFC 9112's rule that
|
|
185
|
-
* `chunked` comes last. `gzip, chunked` is fine and `chunked, gzip` is not: with a coding applied
|
|
186
|
-
* after the framing one, nothing can say where the body ends, and node answers 400 rather than
|
|
187
|
-
* guess. µWS guesses, and what it guesses wrong becomes the next request on the connection.
|
|
188
|
-
*
|
|
189
|
-
* Read per header rather than over the joined value, so a request splitting the list across two
|
|
190
|
-
* transfer-encoding headers is refused even when the codings would be legal joined up. That is
|
|
191
|
-
* stricter than node by a hair, on a shape nothing sends, and stricter is the safe direction here.
|
|
192
|
-
*
|
|
193
|
-
* @param {string} value one transfer-encoding header, as uWS hands it over
|
|
194
|
-
* @returns {boolean}
|
|
195
|
-
*/
|
|
196
|
-
function endsWithChunked(value) {
|
|
197
|
-
const last = value.slice(value.lastIndexOf(",") + 1).trim();
|
|
198
|
-
// a coding may carry parameters, which are not part of its name
|
|
199
|
-
const semicolon = last.indexOf(";");
|
|
200
|
-
if ((semicolon === -1 ? last : last.slice(0, semicolon)).trim().toLowerCase() !== "chunked") {
|
|
201
|
-
return false;
|
|
202
|
-
}
|
|
203
|
-
// and only once. "chunked, chunked" ends with it and is still nonsense: a sender may not frame
|
|
204
|
-
// a body twice, and where node refuses the request outright µWS frames it as one chunked body
|
|
205
|
-
// and reads whatever follows as the next request on the connection
|
|
206
|
-
const codings = value.split(",");
|
|
207
|
-
let chunkedCount = 0;
|
|
208
|
-
for (const coding of codings) {
|
|
209
|
-
const parameter = coding.indexOf(";");
|
|
210
|
-
if ((parameter === -1 ? coding : coding.slice(0, parameter)).trim().toLowerCase() === "chunked") {
|
|
211
|
-
chunkedCount++;
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
return chunkedCount === 1;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Whether a Connection header says the connection ends with this response.
|
|
219
|
-
*
|
|
220
|
-
* It is a list, and "keep-alive, close" closes as much as "close" alone does. Compared against an
|
|
221
|
-
* exact "close", this server kept a connection the client had said it was done with, and then read
|
|
222
|
-
* the bytes after it as another request: node closes there, so the two disagreed on how many
|
|
223
|
-
* requests the same bytes carried, which is what a desync is.
|
|
224
|
-
*
|
|
225
|
-
* Written as a scan rather than a split and a lowercase, because almost every request that carries
|
|
226
|
-
* this header carries "keep-alive", and both of those allocate per request.
|
|
227
|
-
*
|
|
228
|
-
* @param {string} value as µWS hands it over
|
|
229
|
-
* @returns {boolean}
|
|
230
|
-
*/
|
|
231
|
-
function saysClose(value) {
|
|
232
|
-
// what clients actually send, almost always: two interned compares answer before the scan
|
|
233
|
-
if (value === "keep-alive") {
|
|
234
|
-
return false;
|
|
235
|
-
}
|
|
236
|
-
if (value === "close") {
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
const length = value.length;
|
|
240
|
-
let at = 0;
|
|
241
|
-
while (at < length) {
|
|
242
|
-
while (at < length && (value.charCodeAt(at) === 0x20 || value.charCodeAt(at) === 0x09)) {
|
|
243
|
-
at++;
|
|
244
|
-
}
|
|
245
|
-
const start = at;
|
|
246
|
-
while (at < length && value.charCodeAt(at) !== 0x2c) {
|
|
247
|
-
at++;
|
|
248
|
-
}
|
|
249
|
-
let end = at;
|
|
250
|
-
while (end > start && (value.charCodeAt(end - 1) === 0x20 || value.charCodeAt(end - 1) === 0x09)) {
|
|
251
|
-
end--;
|
|
252
|
-
}
|
|
253
|
-
if (
|
|
254
|
-
end - start === 5 &&
|
|
255
|
-
(value.charCodeAt(start) | 0x20) === 0x63 &&
|
|
256
|
-
(value.charCodeAt(start + 1) | 0x20) === 0x6c &&
|
|
257
|
-
(value.charCodeAt(start + 2) | 0x20) === 0x6f &&
|
|
258
|
-
(value.charCodeAt(start + 3) | 0x20) === 0x73 &&
|
|
259
|
-
(value.charCodeAt(start + 4) | 0x20) === 0x65
|
|
260
|
-
) {
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
at++;
|
|
264
|
-
}
|
|
265
|
-
return false;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* The path of the url a request carries right now, without the query.
|
|
270
|
-
*
|
|
271
|
-
* Express reads it off req.url on every access, so a middleware that assigns req.url is seen by
|
|
272
|
-
* whatever runs next, the callback after it in the same route included: the router only takes a
|
|
273
|
-
* rewrite over at its next hop. The cached field answers while the two agree, which is every read
|
|
274
|
-
* of a request nobody rewrote.
|
|
275
|
-
*
|
|
276
|
-
* @param {any} req
|
|
277
|
-
* @returns {string}
|
|
278
|
-
*/
|
|
279
|
-
function currentPath(req) {
|
|
280
|
-
const url = req.url;
|
|
281
|
-
if (url === req._lastUrl) {
|
|
282
|
-
return req._path;
|
|
283
|
-
}
|
|
284
|
-
const query = url.indexOf("?");
|
|
285
|
-
return query === -1 ? url : url.slice(0, query);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
/**
|
|
289
|
-
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
290
|
-
*
|
|
291
|
-
* uWS trims the spaces around the value and then takes whatever is left, so "", "abc", "+1", "-1",
|
|
292
|
-
* "0x10" and "1e2" all arrive here. Every one of them makes uWS frame the request as carrying no
|
|
293
|
-
* body, and what the client sent as a body is then read as the next request on the connection.
|
|
294
|
-
* Node's parser refuses all of them outright, and so does this.
|
|
295
|
-
*
|
|
296
|
-
* @param {string} value as uWS hands it over
|
|
297
|
-
* @returns {boolean}
|
|
298
|
-
*/
|
|
299
|
-
function isByteCount(value) {
|
|
300
|
-
if (value.length === 0) {
|
|
301
|
-
return false;
|
|
302
|
-
}
|
|
303
|
-
for (let i = 0; i < value.length; i++) {
|
|
304
|
-
const code = value.charCodeAt(i);
|
|
305
|
-
if (code < 0x30 || code > 0x39) {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
// A count nothing can represent is not a count. Node refuses one that overflows, and µWS framed
|
|
310
|
-
// the request as if it had said something else, which put the bytes after it in a request of
|
|
311
|
-
// their own. The length test first, so an ordinary value never parses.
|
|
312
|
-
if (value.length > 15 && Number(value) > Number.MAX_SAFE_INTEGER) {
|
|
313
|
-
return false;
|
|
314
|
-
}
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
27
|
+
const { isIP } = require("node:net");
|
|
28
|
+
const { LazyReadable } = require("./lazy-readable.js");
|
|
29
|
+
const {
|
|
30
|
+
asMessage,
|
|
31
|
+
formatIPv6,
|
|
32
|
+
isMappedIPv4,
|
|
33
|
+
mapsIPv4Peer,
|
|
34
|
+
emptyAddress,
|
|
35
|
+
discardedDuplicates,
|
|
36
|
+
KNOWN_METHODS,
|
|
37
|
+
isAsciiTarget,
|
|
38
|
+
endsWithChunked,
|
|
39
|
+
saysClose,
|
|
40
|
+
currentPath,
|
|
41
|
+
isByteCount
|
|
42
|
+
} = require("./request-utils.js");
|
|
317
43
|
|
|
318
44
|
// Whose headers the shared collector below is filling. uWS's forEach is synchronous and runs no
|
|
319
45
|
// user code, so the handoff cannot interleave; module-level so the callback exists once instead
|
|
320
46
|
// of once per request.
|
|
321
47
|
let currentRequest = null;
|
|
322
48
|
|
|
323
|
-
/**
|
|
324
|
-
* A Readable that has not been built yet.
|
|
325
|
-
*
|
|
326
|
-
* Every request pays for the stream and almost none of them use it: a GET carries no body, and the
|
|
327
|
-
* bodies that do arrive are collected by µWS and handed to the parsers without the stream being
|
|
328
|
-
* touched. Measured on this machine, running Readable's constructor costs about 90ns of the 900ns
|
|
329
|
-
* a hello-world request costs in total, which is a tenth of it for a facility nobody asked for.
|
|
330
|
-
*
|
|
331
|
-
* So the chain says Readable and the constructor does not run. `Request extends LazyReadable`, and
|
|
332
|
-
* LazyReadable's prototype is Readable's, which keeps `req instanceof Readable` true and every
|
|
333
|
-
* Readable method reachable; what is missing is `_readableState`, and that is built on the first
|
|
334
|
-
* touch. A derived class cannot skip its super() call, but a base class with nothing in it costs
|
|
335
|
-
* nothing to call.
|
|
336
|
-
*
|
|
337
|
-
* The wrapping below is generated rather than written out, and deliberately: every own member of
|
|
338
|
-
* Readable's prototype gets a version that materialises first, so there is no list to keep in step
|
|
339
|
-
* and no door left unguarded. Missing one would not be a slow path, it would be a TypeError on
|
|
340
|
-
* `undefined._readableState` in whatever corner of a stream nobody tested.
|
|
341
|
-
*/
|
|
342
|
-
class LazyReadableBase {}
|
|
343
|
-
Object.setPrototypeOf(LazyReadableBase.prototype, Readable.prototype);
|
|
344
|
-
Object.setPrototypeOf(LazyReadableBase, Readable);
|
|
345
|
-
|
|
346
|
-
// what the chain says at runtime, said again for the type checker, which cannot see a prototype
|
|
347
|
-
// being reassigned: everything a Readable offers is reachable from a Request, and is a Readable's
|
|
348
|
-
const LazyReadable = /** @type {typeof Readable} */ (/** @type {unknown} */ (LazyReadableBase));
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Builds the stream this object has been pretending to be. Idempotent: everything that can be
|
|
352
|
-
* reached from outside goes through it, so it is called far more often than it does anything.
|
|
353
|
-
*
|
|
354
|
-
* EventEmitter's init keeps an _events that is already there, so listeners added before this
|
|
355
|
-
* survive it.
|
|
356
|
-
*
|
|
357
|
-
* @param {any} stream
|
|
358
|
-
*/
|
|
359
|
-
function materialise(stream) {
|
|
360
|
-
if (stream._readableState === undefined) {
|
|
361
|
-
Readable.call(stream, READABLE_OPTIONS);
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
for (const member of [
|
|
366
|
-
...Object.getOwnPropertyNames(Readable.prototype),
|
|
367
|
-
...Object.getOwnPropertySymbols(Readable.prototype)
|
|
368
|
-
]) {
|
|
369
|
-
// the constructor is not a door, and `readable` is handled below because a request writes it
|
|
370
|
-
// and writing it must not build the very thing this is avoiding
|
|
371
|
-
if (member === "constructor" || member === "readable") {
|
|
372
|
-
continue;
|
|
373
|
-
}
|
|
374
|
-
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Readable.prototype, member));
|
|
375
|
-
if (typeof descriptor.value === "function") {
|
|
376
|
-
const inner = descriptor.value;
|
|
377
|
-
Object.defineProperty(LazyReadableBase.prototype, member, {
|
|
378
|
-
...descriptor,
|
|
379
|
-
/** @this {any} @param {...any} args */
|
|
380
|
-
value: function (...args) {
|
|
381
|
-
materialise(this);
|
|
382
|
-
return inner.apply(this, args);
|
|
383
|
-
}
|
|
384
|
-
});
|
|
385
|
-
} else if (descriptor.get || descriptor.set) {
|
|
386
|
-
const innerGet = descriptor.get;
|
|
387
|
-
const innerSet = descriptor.set;
|
|
388
|
-
Object.defineProperty(LazyReadableBase.prototype, member, {
|
|
389
|
-
...descriptor,
|
|
390
|
-
get: innerGet
|
|
391
|
-
? /** @this {any} */ function () {
|
|
392
|
-
materialise(this);
|
|
393
|
-
return innerGet.call(this);
|
|
394
|
-
}
|
|
395
|
-
: undefined,
|
|
396
|
-
set: innerSet
|
|
397
|
-
? /** @this {any} @param {any} value */ function (value) {
|
|
398
|
-
materialise(this);
|
|
399
|
-
innerSet.call(this, value);
|
|
400
|
-
}
|
|
401
|
-
: undefined
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
const nodeReadable = /** @type {PropertyDescriptor} */ (
|
|
407
|
-
Object.getOwnPropertyDescriptor(Readable.prototype, "readable")
|
|
408
|
-
);
|
|
409
|
-
|
|
410
|
-
// `readable` on its own: a request sets it while it is being built, and node's setter is a no-op
|
|
411
|
-
// without the state anyway, so the flag is kept as a plain field until there is a stream to ask
|
|
412
|
-
Object.defineProperty(LazyReadableBase.prototype, "readable", {
|
|
413
|
-
configurable: true,
|
|
414
|
-
enumerable: false,
|
|
415
|
-
/** @this {any} */
|
|
416
|
-
get: function () {
|
|
417
|
-
return this._readableState === undefined
|
|
418
|
-
? this._readableFlag === true
|
|
419
|
-
: /** @type {any} */ (nodeReadable.get).call(this);
|
|
420
|
-
},
|
|
421
|
-
/** @this {any} @param {any} value */
|
|
422
|
-
set: function (value) {
|
|
423
|
-
if (this._readableState === undefined) {
|
|
424
|
-
this._readableFlag = !!value;
|
|
425
|
-
return;
|
|
426
|
-
}
|
|
427
|
-
/** @type {any} */ (nodeReadable.set).call(this, value);
|
|
428
|
-
}
|
|
429
|
-
});
|
|
430
|
-
|
|
431
49
|
module.exports = class Request extends LazyReadable {
|
|
432
50
|
/** @type {Record<string, any>|null} */
|
|
433
51
|
#cachedHeaders = null;
|
|
@@ -438,10 +56,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
438
56
|
/**
|
|
439
57
|
* Every header, flat: name then value, name then value.
|
|
440
58
|
*
|
|
441
|
-
* An array of pairs meant one array
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
* them in its forEach, so readers compare without lowering again.
|
|
59
|
+
* An array of pairs meant one array per header on every request, and a request carries eight or
|
|
60
|
+
* ten, so everything that reads this walks it two at a time. The names are lowercase by
|
|
61
|
+
* contract: uWS lowers them on the wire and the node shim lowers them in its forEach.
|
|
445
62
|
*
|
|
446
63
|
* @type {string[]}
|
|
447
64
|
*/
|
|
@@ -458,10 +75,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
458
75
|
|
|
459
76
|
// `body` is deliberately not declared here. A class field would put the property on every
|
|
460
77
|
// request, and on Express there is none until a body parser assigns one. `"body" in req` is how
|
|
461
|
-
// a library asks whether the body
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// where the rest of the public request surface is described.
|
|
78
|
+
// a library asks whether the body was read, and tRPC's express adapter does exactly that:
|
|
79
|
+
// answering yes turned every mutation into "Unexpected end of JSON input". Its type is in
|
|
80
|
+
// types.d.ts, with the rest of the public request surface.
|
|
465
81
|
|
|
466
82
|
/**
|
|
467
83
|
* The response this request arrived with, linked so either reaches the other.
|
|
@@ -619,22 +235,21 @@ module.exports = class Request extends LazyReadable {
|
|
|
619
235
|
_sawContentLength;
|
|
620
236
|
|
|
621
237
|
/**
|
|
622
|
-
* Whether this request must not be routed at all. Node's parser refuses each of these
|
|
623
|
-
*
|
|
624
|
-
*
|
|
238
|
+
* Whether this request must not be routed at all. Node's parser refuses each of these and
|
|
239
|
+
* answers 400; every one is a way for bytes the client did not send as a request to be served
|
|
240
|
+
* as one, which is request smuggling.
|
|
625
241
|
*
|
|
626
242
|
* a repeated content-length uWS frames on the first and drops the rest, so a proxy in
|
|
627
|
-
* front reading the last one
|
|
628
|
-
*
|
|
243
|
+
* front reading the last one forwards bytes uWS then answers
|
|
244
|
+
* as a second, pipelined request
|
|
629
245
|
* one that is not a byte count uWS keeps whatever is left after trimming, an empty value
|
|
630
|
-
* included, and frames the request as carrying no body
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* a method nobody defines uWS takes any token as the method, so anything
|
|
634
|
-
*
|
|
635
|
-
*
|
|
636
|
-
*
|
|
637
|
-
* reads them and answers 400, uWS served them. See KNOWN_METHODS
|
|
246
|
+
* included, and frames the request as carrying no body, which
|
|
247
|
+
* turns the body the client sent into that second request.
|
|
248
|
+
* See isByteCount
|
|
249
|
+
* a method nobody defines uWS takes any token as the method, so anything followed by a
|
|
250
|
+
* space and a path is a request line to it. With no
|
|
251
|
+
* content-length and no transfer-encoding there is no body, so
|
|
252
|
+
* the bytes after it are the next request. See KNOWN_METHODS
|
|
638
253
|
*
|
|
639
254
|
* Declared for the same reason as rawIp.
|
|
640
255
|
*
|
|
@@ -690,9 +305,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
690
305
|
* @param {any} req the uWS request, readable only during this call
|
|
691
306
|
* @param {any} res the uWS response
|
|
692
307
|
* @param {any} app the application or router this request arrived at
|
|
693
|
-
* @param {any} [preset] a literal native registration's constants:
|
|
694
|
-
*
|
|
695
|
-
*
|
|
308
|
+
* @param {any} [preset] a literal native registration's constants: uWS matched the URL byte for
|
|
309
|
+
* byte against that exact pattern and dispatched by method, so path, method and what derives
|
|
310
|
+
* from them are known without asking
|
|
696
311
|
* @param {any} [skipHolder] where a granted header skip lives: the preset itself for a
|
|
697
312
|
* literal registration, a holder of its own for a parameterised one
|
|
698
313
|
*/
|
|
@@ -705,33 +320,25 @@ module.exports = class Request extends LazyReadable {
|
|
|
705
320
|
// The chain behind this registration provably never reads a header, so instead of
|
|
706
321
|
// copying them all out of uWS the constructor asks for the ones that steer the
|
|
707
322
|
// framework itself: body framing, keep-alive, and the conditional pair. A GET that
|
|
708
|
-
//
|
|
709
|
-
// whole picture, so it takes the full copy.
|
|
323
|
+
// declares a body takes the full copy.
|
|
710
324
|
//
|
|
711
|
-
// A handful of named reads
|
|
712
|
-
//
|
|
713
|
-
// the
|
|
714
|
-
//
|
|
715
|
-
// headers, 1.61 at eight, 2.90 at sixteen. They do not cross, and the gap widens
|
|
716
|
-
// exactly where real traffic lives, since a browser sends a dozen or more. The body
|
|
717
|
-
// case pays two reads and then copies anyway, which is 0.2us on a request that is
|
|
718
|
-
// about to read a body.
|
|
325
|
+
// A handful of named reads beats one forEach: measured at seven reads they are flat at
|
|
326
|
+
// 0.75us however many headers are on the wire, since each one is a napi crossing, while
|
|
327
|
+
// the copy pays a hop back into JS per header and grows, 1.16us at four headers, 1.61
|
|
328
|
+
// at eight, 2.90 at sixteen. The body case pays two reads and then copies anyway, 0.2us.
|
|
719
329
|
//
|
|
720
|
-
// accept is not read: nothing on a granted chain consumes it, the error and 404
|
|
721
|
-
//
|
|
330
|
+
// accept is not read: nothing on a granted chain consumes it, the error and 404 pages
|
|
331
|
+
// are fixed HTML that never negotiate.
|
|
722
332
|
const length = req.getHeader("content-length");
|
|
723
333
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
724
334
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
725
|
-
// getHeader only
|
|
726
|
-
//
|
|
727
|
-
//
|
|
335
|
+
// getHeader only returns the first of a repeated header, so a duplicate cannot be seen
|
|
336
|
+
// from here and has to be refused rather than routed, see _mustRefuse. Anything that
|
|
337
|
+
// says a word about framing takes the full copy instead.
|
|
728
338
|
//
|
|
729
339
|
// One shape stays invisible here, a content-length present with an empty value: uWS
|
|
730
|
-
// answers "" for that and for a header
|
|
731
|
-
//
|
|
732
|
-
// the second, so this server stays consistent with itself either way. The full copy
|
|
733
|
-
// below does refuse it, which is every request except a GET whose whole chain provably
|
|
734
|
-
// reads no header at all.
|
|
340
|
+
// answers "" for that and for a header never sent, and nothing in its API tells them
|
|
341
|
+
// apart. It frames both as carrying no body. The full copy below does refuse it.
|
|
735
342
|
if (length !== "" || transferEncoding !== "") {
|
|
736
343
|
currentRequest = this;
|
|
737
344
|
this._req.forEach(Request.#collectHeader);
|
|
@@ -772,11 +379,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
772
379
|
}
|
|
773
380
|
this.routeCount = 1;
|
|
774
381
|
this.app = app;
|
|
775
|
-
// both forms are kept
|
|
776
|
-
//
|
|
777
|
-
//
|
|
778
|
-
// neither, the native call is not made at all: the framework's own answers, the 404
|
|
779
|
-
// included, are written from the path alone
|
|
382
|
+
// both forms are kept because both are asked for: the query with its "?" goes into req.url,
|
|
383
|
+
// and req.query parses the raw one. When the chain provably reads neither, the native call
|
|
384
|
+
// is not made at all: the framework's own answers are written from the path alone
|
|
780
385
|
if (skipHolder !== undefined && skipHolder.skipQuery) {
|
|
781
386
|
this._rawQuery = "";
|
|
782
387
|
this.urlQuery = "";
|
|
@@ -831,12 +436,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
831
436
|
this._isHead = skipHolder.isHead;
|
|
832
437
|
} else {
|
|
833
438
|
this.method = rawMethod.toUpperCase();
|
|
834
|
-
// node's parser knows a fixed set and refuses everything else
|
|
439
|
+
// node's parser knows a fixed set and refuses everything else, uWS takes the token
|
|
835
440
|
// as it finds it, so a request line is anything with a space in it. Compared before
|
|
836
|
-
// the uppercasing on purpose: a method is case sensitive, node refuses "post"
|
|
837
|
-
//
|
|
838
|
-
// route anyway, since a route can only be registered for one of these, see the loop
|
|
839
|
-
// that builds the verb methods at the end of router.js
|
|
441
|
+
// the uppercasing on purpose: a method is case sensitive, node refuses "post" and
|
|
442
|
+
// uWS folds it to POST and serves it
|
|
840
443
|
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
841
444
|
this._mustRefuse = true;
|
|
842
445
|
}
|
|
@@ -857,12 +460,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
857
460
|
// Two Sets per request, for two things almost no request needs.
|
|
858
461
|
//
|
|
859
462
|
// _matchedMethods collects the verbs a path answers so an OPTIONS request can be told what
|
|
860
|
-
// they are, and every
|
|
861
|
-
// the requests that are one.
|
|
463
|
+
// they are, and every reader asks _isOptions first, so it is built only for those.
|
|
862
464
|
//
|
|
863
|
-
// _paramCalled remembers, per router, what each app.param() callback was called with
|
|
864
|
-
//
|
|
865
|
-
// The router builds it the first time it has something to put in it.
|
|
465
|
+
// _paramCalled remembers, per router, what each app.param() callback was called with, so
|
|
466
|
+
// only an application using app.param wants it. The router builds it when it has something.
|
|
866
467
|
this._matchedMethods = this._isOptions ? new Set() : null;
|
|
867
468
|
this._paramCalled = null;
|
|
868
469
|
// null for the same reason as the two above: a request that never enters a mount never
|
|
@@ -892,10 +493,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
892
493
|
}
|
|
893
494
|
|
|
894
495
|
// A body exists on the wire only when the request declares one, content-length or
|
|
895
|
-
// transfer-encoding, whatever the verb, and that
|
|
896
|
-
//
|
|
897
|
-
// request said nothing the headers had not already said; the setting still gates the
|
|
898
|
-
// body parsers, which is where it matters
|
|
496
|
+
// transfer-encoding, whatever the verb, and that was spotted during the header copy. The
|
|
497
|
+
// verb list this used to read said nothing the headers had not already said
|
|
899
498
|
if (/** @type {any} */ (this)._declaresBody) {
|
|
900
499
|
this._subscribeBody();
|
|
901
500
|
} else {
|
|
@@ -1004,8 +603,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1004
603
|
* a visitor who has gone away can be stopped. `@angular/ssr` reads it when it builds a web
|
|
1005
604
|
* Request out of this one, which is how an SSR render learns to give up.
|
|
1006
605
|
*
|
|
1007
|
-
* Made on the first ask
|
|
1008
|
-
* AbortController each would be an allocation nobody reads.
|
|
606
|
+
* Made on the first ask: most requests never look at it.
|
|
1009
607
|
*
|
|
1010
608
|
* @returns {AbortSignal}
|
|
1011
609
|
*/
|
|
@@ -1095,11 +693,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1095
693
|
if (this._mountSlash !== true) {
|
|
1096
694
|
return this._originalPath.slice(0, this._consumed);
|
|
1097
695
|
}
|
|
1098
|
-
// Express drops one trailing slash off each mount before joining them, so this is a join
|
|
1099
|
-
//
|
|
1100
|
-
//
|
|
1101
|
-
// appended to that rather than to the original. Only a RegExp mount can take a trailing
|
|
1102
|
-
// slash, a registered path having had it removed, so almost every request answers above.
|
|
696
|
+
// Express drops one trailing slash off each mount before joining them, so this is a join of
|
|
697
|
+
// the pieces and not one slice of the path: a RegExp mount ending in "/" matched against
|
|
698
|
+
// "/a//b" takes "/a/" and reads back as "/a". Only a RegExp mount can take a trailing slash
|
|
1103
699
|
let out = "";
|
|
1104
700
|
let at = 0;
|
|
1105
701
|
for (let taken of this._stack) {
|
|
@@ -1324,37 +920,33 @@ module.exports = class Request extends LazyReadable {
|
|
|
1324
920
|
* object, so a key like "__proto__" cannot reach Object.prototype. No setter, so assigning to
|
|
1325
921
|
* req.query throws as it does on Express.
|
|
1326
922
|
*
|
|
1327
|
-
* Every read answers a new object, because express
|
|
1328
|
-
*
|
|
1329
|
-
*
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
* the parse cached and handed out as itself, the sanitised value leaked into req.query here and
|
|
1333
|
-
* a handler written against express read a trimmed value where express gives it the raw one.
|
|
923
|
+
* Every read answers a new object, because express re-parses on every read and hands one back
|
|
924
|
+
* too. So req.query is never the object another reader holds, and a write to a key of it is
|
|
925
|
+
* gone by the next read. That is how express-validator's sanitisers behave: `.trim()` on a
|
|
926
|
+
* query parameter changes nothing an ordinary handler sees. With the parse cached and handed
|
|
927
|
+
* out as itself, the sanitised value leaked into req.query here.
|
|
1334
928
|
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
929
|
+
* So there is no cache of the object: the fresh object comes from the raw string, not from
|
|
930
|
+
* copying a kept parse. Parse-once-copy-per-read was the first shape shipped, and the copy was
|
|
931
|
+
* the expensive half: Object.assign between null-prototype objects, which live in V8's
|
|
932
|
+
* dictionary mode, measured 638ns for a two-parameter query where parsing the same string
|
|
933
|
+
* measures 119ns, so +1.5us of CPU per request, which a public arena saw as -8% on its
|
|
934
|
+
* query-carrying rows.
|
|
1341
935
|
*
|
|
1342
|
-
* The default parser
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
936
|
+
* The default parser keeps the decoded pairs of its first parse and replays the stores into a
|
|
937
|
+
* fresh null-prototype object: same output, nothing shared between reads. A repeated key
|
|
938
|
+
* cannot be replayed and re-parses.
|
|
1345
939
|
*
|
|
1346
940
|
* @returns {Record<string, any>}
|
|
1347
941
|
*/
|
|
1348
942
|
get query() {
|
|
1349
943
|
const qp = this.app._hot().queryParserFn;
|
|
1350
|
-
// the vendored default already answers on a bare null prototype, so it goes out as is;
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
// A parser of the application's own is handed what express hands it,
|
|
1354
|
-
//
|
|
1355
|
-
//
|
|
1356
|
-
// express, which may check for null before it reads the string, saw a request that had no
|
|
1357
|
-
// query as one with an empty query. The two built in parsers take the raw string.
|
|
944
|
+
// the vendored default already answers on a bare null prototype, so it goes out as is; any
|
|
945
|
+
// other parser is copied onto one, which kept fast-querystring's result from inspecting as
|
|
946
|
+
// "Empty <[Object: null prototype] {}>" where Express shows the bare form.
|
|
947
|
+
// A parser of the application's own is handed what express hands it, parseurl's `query`:
|
|
948
|
+
// null when the url carries no "?", the text after it otherwise, empty string included.
|
|
949
|
+
// Passing "" for both made a parser written for express see no query as an empty query.
|
|
1358
950
|
if (!qp) {
|
|
1359
951
|
return Object.create(null);
|
|
1360
952
|
}
|
|
@@ -1436,12 +1028,11 @@ module.exports = class Request extends LazyReadable {
|
|
|
1436
1028
|
* The peer address bytes, from the socket or, when the application asked for it, from a PROXY
|
|
1437
1029
|
* protocol preamble the load balancer in front of this server sent ahead of the request.
|
|
1438
1030
|
*
|
|
1439
|
-
* The setting is off by default and has to stay that way.
|
|
1440
|
-
* sends it, with
|
|
1441
|
-
*
|
|
1442
|
-
*
|
|
1443
|
-
*
|
|
1444
|
-
* proxy in front of it.
|
|
1031
|
+
* The setting is off by default and has to stay that way. uWS parses the preamble from whoever
|
|
1032
|
+
* sends it, with no way to restrict who may, so an application that took the address
|
|
1033
|
+
* unconditionally would let any client claim any address: the first sixteen bytes of a
|
|
1034
|
+
* connection are enough to become 10.0.0.1 for a rate limiter or an allow list. Turn it on only
|
|
1035
|
+
* when nothing can reach this server except the proxy in front of it.
|
|
1445
1036
|
*
|
|
1446
1037
|
* @returns {ArrayBuffer} the socket's own address when no preamble arrived
|
|
1447
1038
|
*/
|
|
@@ -1498,11 +1089,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1498
1089
|
} else if (rawIp.byteLength === 16) {
|
|
1499
1090
|
const bytes = new Uint8Array(rawIp);
|
|
1500
1091
|
if (isMappedIPv4(bytes)) {
|
|
1501
|
-
// ::ffff:a.b.c.d,
|
|
1502
|
-
//
|
|
1503
|
-
//
|
|
1504
|
-
// longest run of zeros, and measured 157ns more per request for it. Anything that
|
|
1505
|
-
// reads req.ip pays that once, and morgan reads it on every line it writes.
|
|
1092
|
+
// ::ffff:a.b.c.d, what a dual stack listener hands over for every IPv4 peer, so
|
|
1093
|
+
// nearly every request here. The general path below reaches the same string through
|
|
1094
|
+
// a DataView and a scan for the longest zero run, 157ns more per request
|
|
1506
1095
|
ip = "::ffff:" + bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
|
|
1507
1096
|
} else {
|
|
1508
1097
|
// ipv6
|
|
@@ -1543,13 +1132,12 @@ module.exports = class Request extends LazyReadable {
|
|
|
1543
1132
|
}
|
|
1544
1133
|
|
|
1545
1134
|
/**
|
|
1546
|
-
* Cuts this request loose from the
|
|
1135
|
+
* Cuts this request loose from the uWS response it arrived on, keeping the two things only
|
|
1547
1136
|
* that response could answer.
|
|
1548
1137
|
*
|
|
1549
|
-
* A websocket upgrade hands the request to the socket, which outlives the response
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
* inert stand-in answers anything that asks later.
|
|
1138
|
+
* A websocket upgrade hands the request to the socket, which outlives the response. Reading the
|
|
1139
|
+
* peer address through the freed response is a use after free, so the values are taken while it
|
|
1140
|
+
* is still alive and an inert stand-in answers later.
|
|
1553
1141
|
*/
|
|
1554
1142
|
_detachFromResponse() {
|
|
1555
1143
|
const uwsRes = this._res;
|
|
@@ -1756,12 +1344,12 @@ module.exports = class Request extends LazyReadable {
|
|
|
1756
1344
|
}
|
|
1757
1345
|
|
|
1758
1346
|
/**
|
|
1759
|
-
* The request headers as node presents them: lowercased names, and repeats folded the way
|
|
1760
|
-
*
|
|
1761
|
-
* discardedDuplicates keep only the first value,
|
|
1347
|
+
* The request headers as node presents them: lowercased names, and repeats folded the way node
|
|
1348
|
+
* folds them. Set-Cookie stays an array, Cookie is joined with "; ", the fields listed in
|
|
1349
|
+
* discardedDuplicates keep only the first value, everything else is joined with ", ".
|
|
1762
1350
|
*
|
|
1763
|
-
* Built on first read and cached
|
|
1764
|
-
*
|
|
1351
|
+
* Built on first read and cached: routing works from the raw entries and most requests never
|
|
1352
|
+
* ask for this.
|
|
1765
1353
|
*
|
|
1766
1354
|
* @returns {Record<string, any>}
|
|
1767
1355
|
*/
|