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
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
7
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
8
|
+
you may not use this file except in compliance with the License.
|
|
9
|
+
You may obtain a copy of the License at
|
|
10
|
+
|
|
11
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
12
|
+
|
|
13
|
+
Unless required by applicable law or agreed to in writing, software
|
|
14
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
15
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
16
|
+
See the License for the specific language governing permissions and
|
|
17
|
+
limitations under the License.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const { isIP } = require("node:net");
|
|
21
|
+
|
|
22
|
+
/** @typedef {import("./request.js")} Request */
|
|
23
|
+
|
|
24
|
+
// accepts, type-is, proxy-addr and fresh declare a node IncomingMessage but read only .headers off
|
|
25
|
+
// it. This request is not one, so it is passed as itself and the declared type is stepped around.
|
|
26
|
+
/** @param {Request} req @returns {import("http").IncomingMessage} */
|
|
27
|
+
const asMessage = (req) => /** @type {import("http").IncomingMessage} */ (/** @type {unknown} */ (req));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Writes an address like node's socket.remoteAddress, which is inet_ntop and so RFC 5952: leading
|
|
31
|
+
* zeros dropped, the longest zero run written "::", the last four bytes dotted for a mapped IPv4.
|
|
32
|
+
* Printed in full, req.ip was "0000:0000:0000:0000:0000:0000:0000:0001" where Express says "::1".
|
|
33
|
+
*
|
|
34
|
+
* @param {number[]} groups the eight 16-bit groups, most significant first
|
|
35
|
+
* @returns {string}
|
|
36
|
+
*/
|
|
37
|
+
function formatIPv6(groups) {
|
|
38
|
+
// longest run of zero groups, leftmost on a tie, which is the run inet_ntop replaces
|
|
39
|
+
let bestStart = -1;
|
|
40
|
+
let bestLength = 0;
|
|
41
|
+
for (let i = 0; i < 8; i++) {
|
|
42
|
+
if (groups[i] !== 0) continue;
|
|
43
|
+
let run = 1;
|
|
44
|
+
while (i + run < 8 && groups[i + run] === 0) run++;
|
|
45
|
+
if (run > bestLength) {
|
|
46
|
+
bestStart = i;
|
|
47
|
+
bestLength = run;
|
|
48
|
+
}
|
|
49
|
+
i += run - 1;
|
|
50
|
+
}
|
|
51
|
+
// a single zero group is written as "0", not as "::"
|
|
52
|
+
if (bestLength < 2) {
|
|
53
|
+
bestStart = -1;
|
|
54
|
+
bestLength = 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ::ffff:a.b.c.d, and the deprecated ::a.b.c.d. The test is inet_ntop's own, including that a
|
|
58
|
+
// run of seven leading zeros never reaches it, since group 6 is inside the run by then.
|
|
59
|
+
const mixed =
|
|
60
|
+
bestStart === 0 &&
|
|
61
|
+
(bestLength === 6 || (bestLength === 7 && groups[7] !== 1) || (bestLength === 5 && groups[5] === 0xffff));
|
|
62
|
+
|
|
63
|
+
let out = "";
|
|
64
|
+
for (let i = 0; i < 8; i++) {
|
|
65
|
+
if (bestStart !== -1 && i >= bestStart && i < bestStart + bestLength) {
|
|
66
|
+
if (i === bestStart) out += ":";
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (i !== 0) out += ":";
|
|
70
|
+
if (mixed && i === 6) {
|
|
71
|
+
out += `${groups[6] >> 8}.${groups[6] & 0xff}.${groups[7] >> 8}.${groups[7] & 0xff}`;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
out += groups[i].toString(16);
|
|
75
|
+
}
|
|
76
|
+
// a run reaching the end leaves a trailing group to close the "::"
|
|
77
|
+
if (bestStart !== -1 && bestStart + bestLength === 8) out += ":";
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Whether these sixteen bytes are an IPv4-mapped address, ::ffff:0:0/96: ten zero bytes then
|
|
83
|
+
* 0xffff. Ten comparisons rather than a loop, this runs on every address that is read.
|
|
84
|
+
*
|
|
85
|
+
* @param {Uint8Array} bytes exactly sixteen of them
|
|
86
|
+
* @returns {boolean}
|
|
87
|
+
*/
|
|
88
|
+
function isMappedIPv4(bytes) {
|
|
89
|
+
return (
|
|
90
|
+
bytes[10] === 0xff &&
|
|
91
|
+
bytes[11] === 0xff &&
|
|
92
|
+
bytes[0] === 0 &&
|
|
93
|
+
bytes[1] === 0 &&
|
|
94
|
+
bytes[2] === 0 &&
|
|
95
|
+
bytes[3] === 0 &&
|
|
96
|
+
bytes[4] === 0 &&
|
|
97
|
+
bytes[5] === 0 &&
|
|
98
|
+
bytes[6] === 0 &&
|
|
99
|
+
bytes[7] === 0 &&
|
|
100
|
+
bytes[8] === 0 &&
|
|
101
|
+
bytes[9] === 0
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps it
|
|
107
|
+
* whenever the listener is dual stack, which is every listen() without an IPv4 address. uWS already
|
|
108
|
+
* gives mapped peers as sixteen bytes, four bytes come only from a v4 listener or the node shim.
|
|
109
|
+
*
|
|
110
|
+
* @param {import("./application.js").Application} app the application the request arrived at
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function mapsIPv4Peer(app) {
|
|
114
|
+
const host = app._listenHost;
|
|
115
|
+
return !(host && isIP(host) === 4);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What µWS returns for a proxied address when no PROXY protocol preamble arrived. */
|
|
119
|
+
const emptyAddress = new ArrayBuffer(0);
|
|
120
|
+
|
|
121
|
+
const discardedDuplicates = new Set([
|
|
122
|
+
"age",
|
|
123
|
+
"authorization",
|
|
124
|
+
"content-length",
|
|
125
|
+
"content-type",
|
|
126
|
+
"etag",
|
|
127
|
+
"expires",
|
|
128
|
+
"from",
|
|
129
|
+
"host",
|
|
130
|
+
"if-modified-since",
|
|
131
|
+
"if-unmodified-since",
|
|
132
|
+
"last-modified",
|
|
133
|
+
"location",
|
|
134
|
+
"max-forwards",
|
|
135
|
+
"proxy-authorization",
|
|
136
|
+
"referer",
|
|
137
|
+
"retry-after",
|
|
138
|
+
"server",
|
|
139
|
+
"user-agent"
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
// The methods node's parser accepts, so the set a request can arrive with behind Express. uWS takes
|
|
143
|
+
// any token, so without this `{"a":1}GET /path HTTP/1.1` is a request with `{"A":1}GET` as the
|
|
144
|
+
// method. See _mustRefuse.
|
|
145
|
+
const KNOWN_METHODS = new Set(require("http").METHODS);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether a request target is bytes node's parser would have accepted, printable ASCII only.
|
|
149
|
+
*
|
|
150
|
+
* uWS takes the target as it finds it and decodes it as UTF-8, so `GET /café` arrives with an é in
|
|
151
|
+
* it and an overlong slash arrives as replacement characters. Node answers 400 instead, and it has
|
|
152
|
+
* to: a proxy in front reading the same bytes would disagree about which path was asked for.
|
|
153
|
+
* Control characters are uWS's own to refuse, so this is one comparison per character.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} target the path or the query string, as µWS decoded it
|
|
156
|
+
* @returns {boolean}
|
|
157
|
+
*/
|
|
158
|
+
function isAsciiTarget(target) {
|
|
159
|
+
for (let i = 0; i < target.length; i++) {
|
|
160
|
+
if (target.charCodeAt(i) > 0x7e) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Whether a transfer-encoding leaves the body's length knowable, RFC 9112's rule that `chunked`
|
|
169
|
+
* comes last. `gzip, chunked` is fine, `chunked, gzip` is not, and node answers 400 rather than
|
|
170
|
+
* guess. uWS guesses, and what it guesses wrong becomes the next request on the connection.
|
|
171
|
+
*
|
|
172
|
+
* Read per header, not over the joined value, so a request splitting the list across two headers is
|
|
173
|
+
* refused too. Stricter than node by a hair, on a shape nothing sends.
|
|
174
|
+
*
|
|
175
|
+
* @param {string} value one transfer-encoding header, as uWS hands it over
|
|
176
|
+
* @returns {boolean}
|
|
177
|
+
*/
|
|
178
|
+
function endsWithChunked(value) {
|
|
179
|
+
const last = value.slice(value.lastIndexOf(",") + 1).trim();
|
|
180
|
+
// a coding may carry parameters, which are not part of its name
|
|
181
|
+
const semicolon = last.indexOf(";");
|
|
182
|
+
if ((semicolon === -1 ? last : last.slice(0, semicolon)).trim().toLowerCase() !== "chunked") {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
// and only once. "chunked, chunked" ends with it and is still nonsense: a sender may not frame
|
|
186
|
+
// a body twice, and uWS reads whatever follows as the next request on the connection
|
|
187
|
+
const codings = value.split(",");
|
|
188
|
+
let chunkedCount = 0;
|
|
189
|
+
for (const coding of codings) {
|
|
190
|
+
const parameter = coding.indexOf(";");
|
|
191
|
+
if ((parameter === -1 ? coding : coding.slice(0, parameter)).trim().toLowerCase() === "chunked") {
|
|
192
|
+
chunkedCount++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return chunkedCount === 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether a Connection header says the connection ends with this response.
|
|
200
|
+
*
|
|
201
|
+
* It is a list, and "keep-alive, close" closes as much as "close" alone. Compared against an exact
|
|
202
|
+
* "close", this server kept a connection the client was done with and read the bytes after it as
|
|
203
|
+
* another request, which is a desync.
|
|
204
|
+
*
|
|
205
|
+
* A scan rather than a split and a lowercase: almost every request carries "keep-alive" here, and
|
|
206
|
+
* both of those allocate per request.
|
|
207
|
+
*
|
|
208
|
+
* @param {string} value as µWS hands it over
|
|
209
|
+
* @returns {boolean}
|
|
210
|
+
*/
|
|
211
|
+
function saysClose(value) {
|
|
212
|
+
// what clients actually send, almost always: two interned compares answer before the scan
|
|
213
|
+
if (value === "keep-alive") {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
if (value === "close") {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
const length = value.length;
|
|
220
|
+
let at = 0;
|
|
221
|
+
while (at < length) {
|
|
222
|
+
while (at < length && (value.charCodeAt(at) === 0x20 || value.charCodeAt(at) === 0x09)) {
|
|
223
|
+
at++;
|
|
224
|
+
}
|
|
225
|
+
const start = at;
|
|
226
|
+
while (at < length && value.charCodeAt(at) !== 0x2c) {
|
|
227
|
+
at++;
|
|
228
|
+
}
|
|
229
|
+
let end = at;
|
|
230
|
+
while (end > start && (value.charCodeAt(end - 1) === 0x20 || value.charCodeAt(end - 1) === 0x09)) {
|
|
231
|
+
end--;
|
|
232
|
+
}
|
|
233
|
+
if (
|
|
234
|
+
end - start === 5 &&
|
|
235
|
+
(value.charCodeAt(start) | 0x20) === 0x63 &&
|
|
236
|
+
(value.charCodeAt(start + 1) | 0x20) === 0x6c &&
|
|
237
|
+
(value.charCodeAt(start + 2) | 0x20) === 0x6f &&
|
|
238
|
+
(value.charCodeAt(start + 3) | 0x20) === 0x73 &&
|
|
239
|
+
(value.charCodeAt(start + 4) | 0x20) === 0x65
|
|
240
|
+
) {
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
at++;
|
|
244
|
+
}
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The path of the url a request carries right now, without the query.
|
|
250
|
+
*
|
|
251
|
+
* Express reads it off req.url on every access, so a middleware that assigns req.url is seen by
|
|
252
|
+
* whatever runs next, the callback after it in the same route included. The cached field answers
|
|
253
|
+
* while the two agree.
|
|
254
|
+
*
|
|
255
|
+
* @param {Request} req
|
|
256
|
+
* @returns {string}
|
|
257
|
+
*/
|
|
258
|
+
function currentPath(req) {
|
|
259
|
+
const url = req.url;
|
|
260
|
+
if (url === req._lastUrl) {
|
|
261
|
+
return req._path;
|
|
262
|
+
}
|
|
263
|
+
const query = url.indexOf("?");
|
|
264
|
+
return query === -1 ? url : url.slice(0, query);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Whether a content-length is a plain count of bytes, which is the only thing RFC 9112 allows.
|
|
269
|
+
*
|
|
270
|
+
* uWS trims the value and takes whatever is left, so "", "abc", "+1", "-1", "0x10" and "1e2" all
|
|
271
|
+
* arrive here, and each one makes uWS frame the request as carrying no body. Node refuses them all.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} value as uWS hands it over
|
|
274
|
+
* @returns {boolean}
|
|
275
|
+
*/
|
|
276
|
+
function isByteCount(value) {
|
|
277
|
+
if (value.length === 0) {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
for (let i = 0; i < value.length; i++) {
|
|
281
|
+
const code = value.charCodeAt(i);
|
|
282
|
+
if (code < 0x30 || code > 0x39) {
|
|
283
|
+
return false;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
// A count nothing can represent is not a count. Node refuses one that overflows, uWS framed the
|
|
287
|
+
// request as something else. The length test first, so an ordinary value never parses.
|
|
288
|
+
if (value.length > 15 && Number(value) > Number.MAX_SAFE_INTEGER) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
asMessage,
|
|
296
|
+
formatIPv6,
|
|
297
|
+
isMappedIPv4,
|
|
298
|
+
mapsIPv4Peer,
|
|
299
|
+
emptyAddress,
|
|
300
|
+
discardedDuplicates,
|
|
301
|
+
KNOWN_METHODS,
|
|
302
|
+
isAsciiTarget,
|
|
303
|
+
endsWithChunked,
|
|
304
|
+
saysClose,
|
|
305
|
+
currentPath,
|
|
306
|
+
isByteCount
|
|
307
|
+
};
|