fulmine.js 5.19.2 → 5.19.4
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 +63 -73
- package/src/cli.js +48 -48
- package/src/cluster.js +18 -27
- package/src/compression.js +141 -112
- package/src/declarative.js +611 -540
- package/src/hot-settings.js +80 -0
- package/src/index.js +11 -17
- package/src/lazy-readable.js +131 -0
- package/src/lazy-writable.js +97 -0
- package/src/middlewares.js +153 -130
- package/src/nest.js +22 -36
- package/src/node-shim.js +19 -16
- package/src/optimizer.js +600 -0
- package/src/options.d.ts +9 -4
- package/src/parse-query.js +3 -3
- package/src/request-utils.js +307 -0
- package/src/request.js +147 -548
- package/src/response-utils.js +88 -0
- package/src/response.js +228 -535
- package/src/route.js +7 -8
- package/src/router-utils.js +998 -0
- package/src/router.js +166 -2170
- package/src/server-shape.js +40 -51
- package/src/server-timing.js +32 -33
- package/src/socket.js +208 -0
- package/src/testing.js +43 -45
- package/src/usage.js +25 -25
- package/src/utils.js +165 -78
- package/src/verify.js +22 -31
- package/src/view.js +6 -8
- package/src/walk.js +581 -0
- package/src/websocket.js +34 -26
- package/src/work.js +22 -28
package/src/request.js
CHANGED
|
@@ -22,414 +22,32 @@ 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
|
-
/** @type {
|
|
50
|
+
/** @type {import("http").IncomingHttpHeaders|null} */
|
|
433
51
|
#cachedHeaders = null;
|
|
434
52
|
|
|
435
53
|
/** @type {Record<string, string[]>|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.
|
|
@@ -566,7 +182,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
566
182
|
/**
|
|
567
183
|
* next() as the router means it: the rest of the route is skipped. res.sendFile reports its
|
|
568
184
|
* failures here, because express reports them to the router and not to the route.
|
|
569
|
-
* @type {((err?:
|
|
185
|
+
* @type {((err?: unknown) => void)|undefined}
|
|
570
186
|
*/
|
|
571
187
|
_leaveRoute;
|
|
572
188
|
|
|
@@ -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
|
*
|
|
@@ -653,13 +268,16 @@ module.exports = class Request extends LazyReadable {
|
|
|
653
268
|
* through the request. Declared rather than left to appear on assignment: runRoute sets it
|
|
654
269
|
* on every request, and an undeclared property is a shape change on each one.
|
|
655
270
|
*
|
|
271
|
+
* Typed loosely for the same reason as `res`: it is set by runRoute rather than here, and the
|
|
272
|
+
* honest `|undefined` would put a check in front of every call.
|
|
273
|
+
*
|
|
656
274
|
* @type {any}
|
|
657
275
|
*/
|
|
658
276
|
next;
|
|
659
277
|
|
|
660
278
|
/**
|
|
661
279
|
* What the chain threw or passed to next(err), waiting for an error handler.
|
|
662
|
-
* @type {
|
|
280
|
+
* @type {unknown}
|
|
663
281
|
*/
|
|
664
282
|
_error;
|
|
665
283
|
|
|
@@ -687,14 +305,15 @@ module.exports = class Request extends LazyReadable {
|
|
|
687
305
|
* because uWS only lends them for this call, everything derived from them waits until something
|
|
688
306
|
* asks, and the body is subscribed to only for the methods that carry one.
|
|
689
307
|
*
|
|
690
|
-
* @param {
|
|
691
|
-
* @param {
|
|
692
|
-
* @param {
|
|
693
|
-
* @param {
|
|
694
|
-
* for byte against that exact pattern and dispatched by
|
|
695
|
-
* derives from them are known without asking
|
|
696
|
-
* @param {
|
|
697
|
-
* literal registration, a holder of its own for a
|
|
308
|
+
* @param {import("uWebSockets.js").HttpRequest} req the uWS request, readable only during this call
|
|
309
|
+
* @param {import("uWebSockets.js").HttpResponse} res the uWS response
|
|
310
|
+
* @param {import("./application.js").Application} app the application this request arrived at
|
|
311
|
+
* @param {import("./router-utils.js").NativePreset} [preset] a literal native registration's
|
|
312
|
+
* constants: uWS matched the URL byte for byte against that exact pattern and dispatched by
|
|
313
|
+
* method, so path, method and what derives from them are known without asking
|
|
314
|
+
* @param {import("./router-utils.js").SkipHolder} [skipHolder] where a granted header skip
|
|
315
|
+
* lives: the preset itself for a literal registration, a holder of its own for a
|
|
316
|
+
* parameterised one
|
|
698
317
|
*/
|
|
699
318
|
constructor(req, res, app, preset, skipHolder) {
|
|
700
319
|
// nothing: the stream is built on the first touch, see LazyReadable
|
|
@@ -705,33 +324,25 @@ module.exports = class Request extends LazyReadable {
|
|
|
705
324
|
// The chain behind this registration provably never reads a header, so instead of
|
|
706
325
|
// copying them all out of uWS the constructor asks for the ones that steer the
|
|
707
326
|
// framework itself: body framing, keep-alive, and the conditional pair. A GET that
|
|
708
|
-
//
|
|
709
|
-
// whole picture, so it takes the full copy.
|
|
327
|
+
// declares a body takes the full copy.
|
|
710
328
|
//
|
|
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.
|
|
329
|
+
// A handful of named reads beats one forEach: measured at seven reads they are flat at
|
|
330
|
+
// 0.75us however many headers are on the wire, since each one is a napi crossing, while
|
|
331
|
+
// the copy pays a hop back into JS per header and grows, 1.16us at four headers, 1.61
|
|
332
|
+
// at eight, 2.90 at sixteen. The body case pays two reads and then copies anyway, 0.2us.
|
|
719
333
|
//
|
|
720
|
-
// accept is not read: nothing on a granted chain consumes it, the error and 404
|
|
721
|
-
//
|
|
334
|
+
// accept is not read: nothing on a granted chain consumes it, the error and 404 pages
|
|
335
|
+
// are fixed HTML that never negotiate.
|
|
722
336
|
const length = req.getHeader("content-length");
|
|
723
337
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
724
338
|
// A content-length of "0" declares no body and used to stay on the cheap side, but
|
|
725
|
-
// getHeader only
|
|
726
|
-
//
|
|
727
|
-
//
|
|
339
|
+
// getHeader only returns the first of a repeated header, so a duplicate cannot be seen
|
|
340
|
+
// from here and has to be refused rather than routed, see _mustRefuse. Anything that
|
|
341
|
+
// says a word about framing takes the full copy instead.
|
|
728
342
|
//
|
|
729
343
|
// 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.
|
|
344
|
+
// answers "" for that and for a header never sent, and nothing in its API tells them
|
|
345
|
+
// apart. It frames both as carrying no body. The full copy below does refuse it.
|
|
735
346
|
if (length !== "" || transferEncoding !== "") {
|
|
736
347
|
currentRequest = this;
|
|
737
348
|
this._req.forEach(Request.#collectHeader);
|
|
@@ -772,11 +383,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
772
383
|
}
|
|
773
384
|
this.routeCount = 1;
|
|
774
385
|
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
|
|
386
|
+
// both forms are kept because both are asked for: the query with its "?" goes into req.url,
|
|
387
|
+
// and req.query parses the raw one. When the chain provably reads neither, the native call
|
|
388
|
+
// is not made at all: the framework's own answers are written from the path alone
|
|
780
389
|
if (skipHolder !== undefined && skipHolder.skipQuery) {
|
|
781
390
|
this._rawQuery = "";
|
|
782
391
|
this.urlQuery = "";
|
|
@@ -831,12 +440,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
831
440
|
this._isHead = skipHolder.isHead;
|
|
832
441
|
} else {
|
|
833
442
|
this.method = rawMethod.toUpperCase();
|
|
834
|
-
// node's parser knows a fixed set and refuses everything else
|
|
443
|
+
// node's parser knows a fixed set and refuses everything else, uWS takes the token
|
|
835
444
|
// 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
|
|
445
|
+
// the uppercasing on purpose: a method is case sensitive, node refuses "post" and
|
|
446
|
+
// uWS folds it to POST and serves it
|
|
840
447
|
if (!KNOWN_METHODS.has(rawMethod)) {
|
|
841
448
|
this._mustRefuse = true;
|
|
842
449
|
}
|
|
@@ -857,12 +464,10 @@ module.exports = class Request extends LazyReadable {
|
|
|
857
464
|
// Two Sets per request, for two things almost no request needs.
|
|
858
465
|
//
|
|
859
466
|
// _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.
|
|
467
|
+
// they are, and every reader asks _isOptions first, so it is built only for those.
|
|
862
468
|
//
|
|
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.
|
|
469
|
+
// _paramCalled remembers, per router, what each app.param() callback was called with, so
|
|
470
|
+
// only an application using app.param wants it. The router builds it when it has something.
|
|
866
471
|
this._matchedMethods = this._isOptions ? new Set() : null;
|
|
867
472
|
this._paramCalled = null;
|
|
868
473
|
// null for the same reason as the two above: a request that never enters a mount never
|
|
@@ -892,11 +497,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
892
497
|
}
|
|
893
498
|
|
|
894
499
|
// 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
|
-
|
|
898
|
-
// body parsers, which is where it matters
|
|
899
|
-
if (/** @type {any} */ (this)._declaresBody) {
|
|
500
|
+
// transfer-encoding, whatever the verb, and that was spotted during the header copy. The
|
|
501
|
+
// verb list this used to read said nothing the headers had not already said
|
|
502
|
+
if (this._declaresBody) {
|
|
900
503
|
this._subscribeBody();
|
|
901
504
|
} else {
|
|
902
505
|
this.receivedData = true;
|
|
@@ -948,7 +551,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
948
551
|
*/
|
|
949
552
|
_rawHeader(name) {
|
|
950
553
|
if (this.#cachedHeaders !== null) {
|
|
951
|
-
|
|
554
|
+
// a string for every name but set-cookie, which the callers never ask for
|
|
555
|
+
return /** @type {string|undefined} */ (this.#cachedHeaders[name]);
|
|
952
556
|
}
|
|
953
557
|
const entries = this.#rawHeadersEntries;
|
|
954
558
|
for (let i = 0, len = entries.length; i < len; i += 2) {
|
|
@@ -969,7 +573,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
969
573
|
*/
|
|
970
574
|
_foldedHeader(name) {
|
|
971
575
|
if (this.#cachedHeaders !== null) {
|
|
972
|
-
return this.#cachedHeaders[name];
|
|
576
|
+
return /** @type {string|undefined} */ (this.#cachedHeaders[name]);
|
|
973
577
|
}
|
|
974
578
|
const entries = this.#rawHeadersEntries;
|
|
975
579
|
let value;
|
|
@@ -1004,8 +608,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1004
608
|
* a visitor who has gone away can be stopped. `@angular/ssr` reads it when it builds a web
|
|
1005
609
|
* Request out of this one, which is how an SSR render learns to give up.
|
|
1006
610
|
*
|
|
1007
|
-
* Made on the first ask
|
|
1008
|
-
* AbortController each would be an allocation nobody reads.
|
|
611
|
+
* Made on the first ask: most requests never look at it.
|
|
1009
612
|
*
|
|
1010
613
|
* @returns {AbortSignal}
|
|
1011
614
|
*/
|
|
@@ -1095,11 +698,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1095
698
|
if (this._mountSlash !== true) {
|
|
1096
699
|
return this._originalPath.slice(0, this._consumed);
|
|
1097
700
|
}
|
|
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.
|
|
701
|
+
// Express drops one trailing slash off each mount before joining them, so this is a join of
|
|
702
|
+
// the pieces and not one slice of the path: a RegExp mount ending in "/" matched against
|
|
703
|
+
// "/a//b" takes "/a/" and reads back as "/a". Only a RegExp mount can take a trailing slash
|
|
1103
704
|
let out = "";
|
|
1104
705
|
let at = 0;
|
|
1105
706
|
for (let taken of this._stack) {
|
|
@@ -1165,7 +766,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1165
766
|
/**
|
|
1166
767
|
* The authority, port included, from Host or from X-Forwarded-Host behind a trusted proxy.
|
|
1167
768
|
* `hostname` is the same value without the port.
|
|
1168
|
-
* @returns {string}
|
|
769
|
+
* @returns {string|undefined} undefined when the request carries no Host
|
|
1169
770
|
*/
|
|
1170
771
|
get host() {
|
|
1171
772
|
return this.#authority;
|
|
@@ -1173,7 +774,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1173
774
|
|
|
1174
775
|
/**
|
|
1175
776
|
* The host without the port.
|
|
1176
|
-
* @returns {string}
|
|
777
|
+
* @returns {string|undefined}
|
|
1177
778
|
*/
|
|
1178
779
|
get hostname() {
|
|
1179
780
|
return this.#host;
|
|
@@ -1244,7 +845,8 @@ module.exports = class Request extends LazyReadable {
|
|
|
1244
845
|
if (!trust(this.parsedIp, 0)) {
|
|
1245
846
|
return proto;
|
|
1246
847
|
}
|
|
1247
|
-
|
|
848
|
+
// folded to one string, as every header but set-cookie is
|
|
849
|
+
const header = /** @type {string|undefined} */ (this.headers["x-forwarded-proto"]) || proto;
|
|
1248
850
|
const index = header.indexOf(",");
|
|
1249
851
|
|
|
1250
852
|
return index !== -1 ? header.slice(0, index).trim() : header.trim();
|
|
@@ -1324,37 +926,33 @@ module.exports = class Request extends LazyReadable {
|
|
|
1324
926
|
* object, so a key like "__proto__" cannot reach Object.prototype. No setter, so assigning to
|
|
1325
927
|
* req.query throws as it does on Express.
|
|
1326
928
|
*
|
|
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.
|
|
929
|
+
* Every read answers a new object, because express re-parses on every read and hands one back
|
|
930
|
+
* too. So req.query is never the object another reader holds, and a write to a key of it is
|
|
931
|
+
* gone by the next read. That is how express-validator's sanitisers behave: `.trim()` on a
|
|
932
|
+
* query parameter changes nothing an ordinary handler sees. With the parse cached and handed
|
|
933
|
+
* out as itself, the sanitised value leaked into req.query here.
|
|
1334
934
|
*
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
935
|
+
* So there is no cache of the object: the fresh object comes from the raw string, not from
|
|
936
|
+
* copying a kept parse. Parse-once-copy-per-read was the first shape shipped, and the copy was
|
|
937
|
+
* the expensive half: Object.assign between null-prototype objects, which live in V8's
|
|
938
|
+
* dictionary mode, measured 638ns for a two-parameter query where parsing the same string
|
|
939
|
+
* measures 119ns, so +1.5us of CPU per request, which a public arena saw as -8% on its
|
|
940
|
+
* query-carrying rows.
|
|
1341
941
|
*
|
|
1342
|
-
* The default parser
|
|
1343
|
-
*
|
|
1344
|
-
*
|
|
942
|
+
* The default parser keeps the decoded pairs of its first parse and replays the stores into a
|
|
943
|
+
* fresh null-prototype object: same output, nothing shared between reads. A repeated key
|
|
944
|
+
* cannot be replayed and re-parses.
|
|
1345
945
|
*
|
|
1346
946
|
* @returns {Record<string, any>}
|
|
1347
947
|
*/
|
|
1348
948
|
get query() {
|
|
1349
949
|
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.
|
|
950
|
+
// the vendored default already answers on a bare null prototype, so it goes out as is; any
|
|
951
|
+
// other parser is copied onto one, which kept fast-querystring's result from inspecting as
|
|
952
|
+
// "Empty <[Object: null prototype] {}>" where Express shows the bare form.
|
|
953
|
+
// A parser of the application's own is handed what express hands it, parseurl's `query`:
|
|
954
|
+
// null when the url carries no "?", the text after it otherwise, empty string included.
|
|
955
|
+
// Passing "" for both made a parser written for express see no query as an empty query.
|
|
1358
956
|
if (!qp) {
|
|
1359
957
|
return Object.create(null);
|
|
1360
958
|
}
|
|
@@ -1436,12 +1034,11 @@ module.exports = class Request extends LazyReadable {
|
|
|
1436
1034
|
* The peer address bytes, from the socket or, when the application asked for it, from a PROXY
|
|
1437
1035
|
* protocol preamble the load balancer in front of this server sent ahead of the request.
|
|
1438
1036
|
*
|
|
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.
|
|
1037
|
+
* The setting is off by default and has to stay that way. uWS parses the preamble from whoever
|
|
1038
|
+
* sends it, with no way to restrict who may, so an application that took the address
|
|
1039
|
+
* unconditionally would let any client claim any address: the first sixteen bytes of a
|
|
1040
|
+
* connection are enough to become 10.0.0.1 for a rate limiter or an allow list. Turn it on only
|
|
1041
|
+
* when nothing can reach this server except the proxy in front of it.
|
|
1445
1042
|
*
|
|
1446
1043
|
* @returns {ArrayBuffer} the socket's own address when no preamble arrived
|
|
1447
1044
|
*/
|
|
@@ -1498,11 +1095,9 @@ module.exports = class Request extends LazyReadable {
|
|
|
1498
1095
|
} else if (rawIp.byteLength === 16) {
|
|
1499
1096
|
const bytes = new Uint8Array(rawIp);
|
|
1500
1097
|
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.
|
|
1098
|
+
// ::ffff:a.b.c.d, what a dual stack listener hands over for every IPv4 peer, so
|
|
1099
|
+
// nearly every request here. The general path below reaches the same string through
|
|
1100
|
+
// a DataView and a scan for the longest zero run, 157ns more per request
|
|
1506
1101
|
ip = "::ffff:" + bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
|
|
1507
1102
|
} else {
|
|
1508
1103
|
// ipv6
|
|
@@ -1520,7 +1115,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1520
1115
|
return ip;
|
|
1521
1116
|
}
|
|
1522
1117
|
|
|
1523
|
-
/** @type {
|
|
1118
|
+
/** @type {import("./socket.js")|null} */
|
|
1524
1119
|
#cachedConnection = null;
|
|
1525
1120
|
|
|
1526
1121
|
/**
|
|
@@ -1528,7 +1123,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1528
1123
|
* stand-in for the pair, as node has one socket for both. Built on first read and kept, so it
|
|
1529
1124
|
* keeps its identity across reads, and kept here as well so that it still answers once the
|
|
1530
1125
|
* response is over and `res.socket` has gone null.
|
|
1531
|
-
* @returns {
|
|
1126
|
+
* @returns {import("./socket.js")}
|
|
1532
1127
|
*/
|
|
1533
1128
|
get connection() {
|
|
1534
1129
|
return (this.#cachedConnection ??= this.res._socketShim());
|
|
@@ -1543,13 +1138,12 @@ module.exports = class Request extends LazyReadable {
|
|
|
1543
1138
|
}
|
|
1544
1139
|
|
|
1545
1140
|
/**
|
|
1546
|
-
* Cuts this request loose from the
|
|
1141
|
+
* Cuts this request loose from the uWS response it arrived on, keeping the two things only
|
|
1547
1142
|
* that response could answer.
|
|
1548
1143
|
*
|
|
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.
|
|
1144
|
+
* A websocket upgrade hands the request to the socket, which outlives the response. Reading the
|
|
1145
|
+
* peer address through the freed response is a use after free, so the values are taken while it
|
|
1146
|
+
* is still alive and an inert stand-in answers later.
|
|
1553
1147
|
*/
|
|
1554
1148
|
_detachFromResponse() {
|
|
1555
1149
|
const uwsRes = this._res;
|
|
@@ -1558,18 +1152,21 @@ module.exports = class Request extends LazyReadable {
|
|
|
1558
1152
|
}
|
|
1559
1153
|
const remotePort = uwsRes.getRemotePort();
|
|
1560
1154
|
const rawIp = this.rawIp;
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1155
|
+
// a stand-in with the members a detached request still asks for, told to the checker once
|
|
1156
|
+
this._res = /** @type {import("uWebSockets.js").HttpResponse} */ (
|
|
1157
|
+
/** @type {unknown} */ ({
|
|
1158
|
+
getRemoteAddress: () => rawIp,
|
|
1159
|
+
// whatever a preamble said is already in rawIp, and asking again is the use after free
|
|
1160
|
+
// this method exists to avoid
|
|
1161
|
+
getProxiedRemoteAddress: () => emptyAddress,
|
|
1162
|
+
getRemotePort: () => remotePort,
|
|
1163
|
+
// a body cannot arrive on an upgraded socket, and a stray reader must not reach µWS
|
|
1164
|
+
onData() {},
|
|
1165
|
+
pause() {},
|
|
1166
|
+
resume() {},
|
|
1167
|
+
close() {}
|
|
1168
|
+
})
|
|
1169
|
+
);
|
|
1573
1170
|
}
|
|
1574
1171
|
|
|
1575
1172
|
/**
|
|
@@ -1653,7 +1250,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1653
1250
|
* acceptable type when called with no arguments
|
|
1654
1251
|
*/
|
|
1655
1252
|
accepts(...types) {
|
|
1656
|
-
return accepts(asMessage(this)).types(.../** @type {
|
|
1253
|
+
return accepts(asMessage(this)).types(.../** @type {string[]} */ (types));
|
|
1657
1254
|
}
|
|
1658
1255
|
|
|
1659
1256
|
/**
|
|
@@ -1662,7 +1259,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1662
1259
|
* @returns {string|string[]|false}
|
|
1663
1260
|
*/
|
|
1664
1261
|
acceptsCharsets(...charsets) {
|
|
1665
|
-
return accepts(asMessage(this)).charsets(.../** @type {
|
|
1262
|
+
return accepts(asMessage(this)).charsets(.../** @type {string[]} */ (charsets));
|
|
1666
1263
|
}
|
|
1667
1264
|
|
|
1668
1265
|
/**
|
|
@@ -1671,7 +1268,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1671
1268
|
* @returns {string|string[]|false}
|
|
1672
1269
|
*/
|
|
1673
1270
|
acceptsEncodings(...encodings) {
|
|
1674
|
-
return accepts(asMessage(this)).encodings(.../** @type {
|
|
1271
|
+
return accepts(asMessage(this)).encodings(.../** @type {string[]} */ (encodings));
|
|
1675
1272
|
}
|
|
1676
1273
|
|
|
1677
1274
|
/**
|
|
@@ -1680,7 +1277,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1680
1277
|
* @returns {string|string[]|false}
|
|
1681
1278
|
*/
|
|
1682
1279
|
acceptsLanguages(...languages) {
|
|
1683
|
-
return accepts(asMessage(this)).languages(.../** @type {
|
|
1280
|
+
return accepts(asMessage(this)).languages(.../** @type {string[]} */ (languages));
|
|
1684
1281
|
}
|
|
1685
1282
|
|
|
1686
1283
|
/**
|
|
@@ -1756,14 +1353,14 @@ module.exports = class Request extends LazyReadable {
|
|
|
1756
1353
|
}
|
|
1757
1354
|
|
|
1758
1355
|
/**
|
|
1759
|
-
* The request headers as node presents them: lowercased names, and repeats folded the way
|
|
1760
|
-
*
|
|
1761
|
-
* discardedDuplicates keep only the first value,
|
|
1356
|
+
* The request headers as node presents them: lowercased names, and repeats folded the way node
|
|
1357
|
+
* folds them. Set-Cookie stays an array, Cookie is joined with "; ", the fields listed in
|
|
1358
|
+
* discardedDuplicates keep only the first value, everything else is joined with ", ".
|
|
1762
1359
|
*
|
|
1763
|
-
* Built on first read and cached
|
|
1764
|
-
*
|
|
1360
|
+
* Built on first read and cached: routing works from the raw entries and most requests never
|
|
1361
|
+
* ask for this.
|
|
1765
1362
|
*
|
|
1766
|
-
* @returns {
|
|
1363
|
+
* @returns {import("http").IncomingHttpHeaders}
|
|
1767
1364
|
*/
|
|
1768
1365
|
get headers() {
|
|
1769
1366
|
// https://nodejs.org/api/http.html#messageheaders
|
|
@@ -1774,6 +1371,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1774
1371
|
// half-filled object cached. A plain object because node's is one and inspect prints the
|
|
1775
1372
|
// difference; Object.hasOwn keeps a header named "constructor" or "toString" from finding
|
|
1776
1373
|
// Object.prototype's member and folding a first value into it.
|
|
1374
|
+
/** @type {import("http").IncomingHttpHeaders} */
|
|
1777
1375
|
const headers = {};
|
|
1778
1376
|
const entries = this.#rawHeadersEntries;
|
|
1779
1377
|
for (let index = 0, len = entries.length; index < len; index += 2) {
|
|
@@ -1789,7 +1387,7 @@ module.exports = class Request extends LazyReadable {
|
|
|
1789
1387
|
if (key === "cookie") {
|
|
1790
1388
|
headers[key] += "; " + value;
|
|
1791
1389
|
} else if (key === "set-cookie") {
|
|
1792
|
-
headers[key].push(value);
|
|
1390
|
+
/** @type {string[]} */ (headers[key]).push(value);
|
|
1793
1391
|
} else {
|
|
1794
1392
|
headers[key] += ", " + value;
|
|
1795
1393
|
}
|
|
@@ -1877,4 +1475,5 @@ module.exports = class Request extends LazyReadable {
|
|
|
1877
1475
|
|
|
1878
1476
|
// req.header is req.get under Express's other name. On the prototype rather than an instance
|
|
1879
1477
|
// field, which wrote one own property per request in the constructor.
|
|
1880
|
-
/** @type {
|
|
1478
|
+
/** @type {{header?: typeof module.exports.prototype.get}} */ (module.exports.prototype).header =
|
|
1479
|
+
module.exports.prototype.get;
|