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.
@@ -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
+ };