fulmine.js 5.1.9 → 5.3.0
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/NOTICE +29 -2
- package/README.md +148 -62
- package/package.json +6 -2
- package/src/application.js +97 -34
- package/src/cli.js +302 -5
- package/src/declarative.js +19 -0
- package/src/index.js +2 -0
- package/src/middlewares.js +97 -25
- package/src/node-shim.js +5 -3
- package/src/options.d.ts +110 -0
- package/src/parse-query.js +19 -2
- package/src/request.js +357 -38
- package/src/response.js +79 -15
- package/src/router.js +686 -160
- package/src/types.d.ts +52 -3
- package/src/usage.js +16 -0
- package/src/utils.js +267 -36
- package/src/view.js +2 -0
- package/src/websocket.js +239 -0
- package/src/worker.js +2 -0
package/src/options.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// The option bags the file-serving and body-parsing paths take, written once and referenced from
|
|
18
|
+
// the JSDoc of the functions that read them. They live in a declaration file rather than as
|
|
19
|
+
// @typedef blocks in the sources because those sources export classes, and a typedef hanging off
|
|
20
|
+
// a `module.exports = class` module makes TypeScript see two unrelated copies of the class.
|
|
21
|
+
|
|
22
|
+
/** What res.sendFile, express.static and res.download all read. The names and defaults are send's. */
|
|
23
|
+
export interface SendFileOptions {
|
|
24
|
+
/** The directory a relative path resolves against, and the boundary nothing may climb out of. */
|
|
25
|
+
root?: string;
|
|
26
|
+
/** Cache-Control's max-age, in milliseconds or as a duration such as "1d". */
|
|
27
|
+
maxAge?: number | string;
|
|
28
|
+
/** Adds Cache-Control: immutable, which is only meaningful next to a long maxAge. */
|
|
29
|
+
immutable?: boolean;
|
|
30
|
+
/** Whether Last-Modified is sent, from the file's mtime. */
|
|
31
|
+
lastModified?: boolean;
|
|
32
|
+
/** Whether an ETag is sent. */
|
|
33
|
+
etag?: boolean;
|
|
34
|
+
/** Whether a Range request is honoured. */
|
|
35
|
+
acceptRanges?: boolean;
|
|
36
|
+
/** Whether Cache-Control is sent at all. */
|
|
37
|
+
cacheControl?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* What to do with a path holding a dotfile. "ignore_files" is one more than send offers:
|
|
40
|
+
* it hides a dotfile that is the last segment while letting a dotted directory through.
|
|
41
|
+
*/
|
|
42
|
+
dotfiles?: "allow" | "deny" | "ignore" | "ignore_files";
|
|
43
|
+
/** Extra headers for the response. */
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
/** Called before the file goes out, to set headers from the path or its stat. */
|
|
46
|
+
setHeaders?: (res: any, path: string, stat: any) => void;
|
|
47
|
+
/** First byte of the window to send. */
|
|
48
|
+
start?: number;
|
|
49
|
+
/** Last byte of the window to send. */
|
|
50
|
+
end?: number;
|
|
51
|
+
/** The path is already encoded, so leave it alone. */
|
|
52
|
+
skipEncodePath?: boolean;
|
|
53
|
+
/** Internal: locals carried through to a view render. */
|
|
54
|
+
_locals?: Record<string, any>;
|
|
55
|
+
/** Internal: the stat the caller already took, so it is not taken twice. */
|
|
56
|
+
_stat?: any;
|
|
57
|
+
/** Internal: the caller computed the ETag itself. */
|
|
58
|
+
_ownEtag?: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** What express.static reads on top of everything res.sendFile takes. */
|
|
62
|
+
export interface StaticOptions extends SendFileOptions {
|
|
63
|
+
/** The file served for a directory, or false to serve none. */
|
|
64
|
+
index?: string | false;
|
|
65
|
+
/** Whether a directory without a trailing slash is redirected to one. */
|
|
66
|
+
redirect?: boolean;
|
|
67
|
+
/** Whether a request this middleware cannot serve moves on instead of being answered. */
|
|
68
|
+
fallthrough?: boolean;
|
|
69
|
+
/** Extensions tried when the path names no file, or false to try none. */
|
|
70
|
+
extensions?: string[] | false;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A body parser's options once its factory has filled in every default it needs. */
|
|
74
|
+
export interface SettledBodyParserOptions extends BodyParserOptions {
|
|
75
|
+
limit: number;
|
|
76
|
+
inflate: boolean;
|
|
77
|
+
/** the string form is turned into a one-element list by the factory */
|
|
78
|
+
type: string[] | ((req: any) => boolean);
|
|
79
|
+
defaultCharset: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** What the body parsers take. The four share these; each names below the ones it alone reads. */
|
|
83
|
+
export interface BodyParserOptions {
|
|
84
|
+
/** The largest body to accept, as bytes or as "100kb". */
|
|
85
|
+
limit?: number | string;
|
|
86
|
+
/** Which content types this parser claims. */
|
|
87
|
+
type?: string | string[] | ((req: any) => boolean);
|
|
88
|
+
/** Runs on the raw bytes before parsing, which is where a signature check belongs. */
|
|
89
|
+
verify?: false | ((req: any, res: any, buf: Buffer, encoding: string) => void);
|
|
90
|
+
/** Whether a compressed body is decompressed rather than refused. */
|
|
91
|
+
inflate?: boolean;
|
|
92
|
+
/** The charset assumed when the request names none. */
|
|
93
|
+
defaultCharset?: string;
|
|
94
|
+
/** json only: refuse a body that is not an object or an array. */
|
|
95
|
+
strict?: boolean;
|
|
96
|
+
/** json only, passed to JSON.parse. */
|
|
97
|
+
reviver?: (key: string, value: any) => any;
|
|
98
|
+
/** urlencoded only: parse with qs rather than the plain parser. */
|
|
99
|
+
extended?: boolean;
|
|
100
|
+
/** urlencoded only: how many parameters to accept. */
|
|
101
|
+
parameterLimit?: number;
|
|
102
|
+
/** urlencoded only, extended only: how deep a nested object may go. */
|
|
103
|
+
depth?: number;
|
|
104
|
+
/** urlencoded only, passed to qs. */
|
|
105
|
+
charsetSentinel?: boolean;
|
|
106
|
+
/** urlencoded only, passed to qs. */
|
|
107
|
+
interpretNumericEntities?: boolean;
|
|
108
|
+
/** Internal: the single type this parser claims, which lets the prologue compare strings. */
|
|
109
|
+
simpleType?: string;
|
|
110
|
+
}
|
package/src/parse-query.js
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2026 Nigro Simone
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
1
17
|
"use strict";
|
|
2
18
|
|
|
3
19
|
/*
|
|
4
|
-
The parser from fast-querystring 1.1.x (
|
|
5
|
-
|
|
20
|
+
The parser from fast-querystring 1.1.x (https://github.com/anonrig/fast-querystring), Copyright
|
|
21
|
+
(c) 2022 Yagiz Nizipli, MIT, whose permission notice is reproduced in full in NOTICE at the root
|
|
22
|
+
of this package. Vendored for one change: the result is a bare
|
|
6
23
|
Object.create(null) instead of the library's Empty-constructor trick. The trick is faster to
|
|
7
24
|
construct but node inspects it as "Empty <[Object: null prototype] {}>", where Express shows
|
|
8
25
|
"[Object: null prototype]", and matching that used to cost an Object.assign copy of every parse
|
package/src/request.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
Copyright 2024 dimden.dev
|
|
3
3
|
Copyright 2026 Nigro Simone
|
|
4
4
|
|
|
5
|
+
This file is derived from Ultimate Express and has been modified.
|
|
6
|
+
|
|
5
7
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
8
|
you may not use this file except in compliance with the License.
|
|
7
9
|
You may obtain a copy of the License at
|
|
@@ -84,6 +86,31 @@ function formatIPv6(groups) {
|
|
|
84
86
|
return out;
|
|
85
87
|
}
|
|
86
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
|
+
|
|
87
114
|
/**
|
|
88
115
|
* Whether node would report an IPv4 peer of this app in mapped form, "::ffff:a.b.c.d". Node maps
|
|
89
116
|
* it whenever the listener is dual stack, which is every listen() not given an IPv4 address to
|
|
@@ -129,7 +156,115 @@ const READABLE_OPTIONS = { highWaterMark: 128 * 1024 };
|
|
|
129
156
|
// of once per request.
|
|
130
157
|
let currentRequest = null;
|
|
131
158
|
|
|
132
|
-
|
|
159
|
+
/**
|
|
160
|
+
* A Readable that has not been built yet.
|
|
161
|
+
*
|
|
162
|
+
* Every request pays for the stream and almost none of them use it: a GET carries no body, and the
|
|
163
|
+
* bodies that do arrive are collected by µWS and handed to the parsers without the stream being
|
|
164
|
+
* touched. Measured on this machine, running Readable's constructor costs about 90ns of the 900ns
|
|
165
|
+
* a hello-world request costs in total, which is a tenth of it for a facility nobody asked for.
|
|
166
|
+
*
|
|
167
|
+
* So the chain says Readable and the constructor does not run. `Request extends LazyReadable`, and
|
|
168
|
+
* LazyReadable's prototype is Readable's, which keeps `req instanceof Readable` true and every
|
|
169
|
+
* Readable method reachable; what is missing is `_readableState`, and that is built on the first
|
|
170
|
+
* touch. A derived class cannot skip its super() call, but a base class with nothing in it costs
|
|
171
|
+
* nothing to call.
|
|
172
|
+
*
|
|
173
|
+
* The wrapping below is generated rather than written out, and deliberately: every own member of
|
|
174
|
+
* Readable's prototype gets a version that materialises first, so there is no list to keep in step
|
|
175
|
+
* and no door left unguarded. Missing one would not be a slow path, it would be a TypeError on
|
|
176
|
+
* `undefined._readableState` in whatever corner of a stream nobody tested.
|
|
177
|
+
*/
|
|
178
|
+
class LazyReadableBase {}
|
|
179
|
+
Object.setPrototypeOf(LazyReadableBase.prototype, Readable.prototype);
|
|
180
|
+
Object.setPrototypeOf(LazyReadableBase, Readable);
|
|
181
|
+
|
|
182
|
+
// what the chain says at runtime, said again for the type checker, which cannot see a prototype
|
|
183
|
+
// being reassigned: everything a Readable offers is reachable from a Request, and is a Readable's
|
|
184
|
+
const LazyReadable = /** @type {typeof Readable} */ (/** @type {unknown} */ (LazyReadableBase));
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Builds the stream this object has been pretending to be. Idempotent: everything that can be
|
|
188
|
+
* reached from outside goes through it, so it is called far more often than it does anything.
|
|
189
|
+
*
|
|
190
|
+
* EventEmitter's init keeps an _events that is already there, so listeners added before this
|
|
191
|
+
* survive it.
|
|
192
|
+
*
|
|
193
|
+
* @param {any} stream
|
|
194
|
+
*/
|
|
195
|
+
function materialise(stream) {
|
|
196
|
+
if (stream._readableState === undefined) {
|
|
197
|
+
Readable.call(stream, READABLE_OPTIONS);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
for (const member of [
|
|
202
|
+
...Object.getOwnPropertyNames(Readable.prototype),
|
|
203
|
+
...Object.getOwnPropertySymbols(Readable.prototype)
|
|
204
|
+
]) {
|
|
205
|
+
// the constructor is not a door, and `readable` is handled below because a request writes it
|
|
206
|
+
// and writing it must not build the very thing this is avoiding
|
|
207
|
+
if (member === "constructor" || member === "readable") {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const descriptor = /** @type {PropertyDescriptor} */ (Object.getOwnPropertyDescriptor(Readable.prototype, member));
|
|
211
|
+
if (typeof descriptor.value === "function") {
|
|
212
|
+
const inner = descriptor.value;
|
|
213
|
+
Object.defineProperty(LazyReadableBase.prototype, member, {
|
|
214
|
+
...descriptor,
|
|
215
|
+
/** @this {any} @param {...any} args */
|
|
216
|
+
value: function (...args) {
|
|
217
|
+
materialise(this);
|
|
218
|
+
return inner.apply(this, args);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
} else if (descriptor.get || descriptor.set) {
|
|
222
|
+
const innerGet = descriptor.get;
|
|
223
|
+
const innerSet = descriptor.set;
|
|
224
|
+
Object.defineProperty(LazyReadableBase.prototype, member, {
|
|
225
|
+
...descriptor,
|
|
226
|
+
get: innerGet
|
|
227
|
+
? /** @this {any} */ function () {
|
|
228
|
+
materialise(this);
|
|
229
|
+
return innerGet.call(this);
|
|
230
|
+
}
|
|
231
|
+
: undefined,
|
|
232
|
+
set: innerSet
|
|
233
|
+
? /** @this {any} @param {any} value */ function (value) {
|
|
234
|
+
materialise(this);
|
|
235
|
+
innerSet.call(this, value);
|
|
236
|
+
}
|
|
237
|
+
: undefined
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const nodeReadable = /** @type {PropertyDescriptor} */ (
|
|
243
|
+
Object.getOwnPropertyDescriptor(Readable.prototype, "readable")
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
// `readable` on its own: a request sets it while it is being built, and node's setter is a no-op
|
|
247
|
+
// without the state anyway, so the flag is kept as a plain field until there is a stream to ask
|
|
248
|
+
Object.defineProperty(LazyReadableBase.prototype, "readable", {
|
|
249
|
+
configurable: true,
|
|
250
|
+
enumerable: false,
|
|
251
|
+
/** @this {any} */
|
|
252
|
+
get: function () {
|
|
253
|
+
return this._readableState === undefined
|
|
254
|
+
? this._readableFlag === true
|
|
255
|
+
: /** @type {any} */ (nodeReadable.get).call(this);
|
|
256
|
+
},
|
|
257
|
+
/** @this {any} @param {any} value */
|
|
258
|
+
set: function (value) {
|
|
259
|
+
if (this._readableState === undefined) {
|
|
260
|
+
this._readableFlag = !!value;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
/** @type {any} */ (nodeReadable.set).call(this, value);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
module.exports = class Request extends LazyReadable {
|
|
133
268
|
/** @type {Record<string, any>|null} */
|
|
134
269
|
#cachedQuery = null;
|
|
135
270
|
|
|
@@ -139,26 +274,53 @@ module.exports = class Request extends Readable {
|
|
|
139
274
|
/** @type {Record<string, string[]>|null} */
|
|
140
275
|
#cachedDistinctHeaders = null;
|
|
141
276
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
277
|
+
/**
|
|
278
|
+
* Every header, flat: name then value, name then value.
|
|
279
|
+
*
|
|
280
|
+
* An array of pairs meant one array allocated per header on every request, and a request
|
|
281
|
+
* carries eight or ten of them, so everything that reads this walks it two at a time. The
|
|
282
|
+
* names are lowercase by contract: uWS lowers them on the wire and the node shim lowers
|
|
283
|
+
* them in its forEach, so readers compare without lowering again.
|
|
284
|
+
*
|
|
285
|
+
* @type {string[]}
|
|
286
|
+
*/
|
|
146
287
|
#rawHeadersEntries = [];
|
|
147
288
|
|
|
148
289
|
/** @type {string|undefined|null} */
|
|
149
290
|
#cachedParsedIp = null;
|
|
150
291
|
|
|
292
|
+
/** Whether backpressure has asked uWS to stop delivering the body for now. */
|
|
151
293
|
#paused = false;
|
|
152
294
|
|
|
153
|
-
|
|
295
|
+
/** A bodyless request whose empty end has not been delivered yet, see the constructor. */
|
|
154
296
|
#emptyBody = false;
|
|
155
297
|
|
|
298
|
+
/**
|
|
299
|
+
* What a body parser left behind, and undefined until one claims the request.
|
|
300
|
+
* @type {any}
|
|
301
|
+
*/
|
|
156
302
|
body;
|
|
157
303
|
|
|
304
|
+
/**
|
|
305
|
+
* The response this request arrived with, linked so either reaches the other.
|
|
306
|
+
*
|
|
307
|
+
* Typed loosely on purpose: it is linked right after construction rather than in the
|
|
308
|
+
* constructor, and the honest `Response|undefined` would put a check in front of every
|
|
309
|
+
* use of a field that is never observed unset.
|
|
310
|
+
*
|
|
311
|
+
* @type {any}
|
|
312
|
+
*/
|
|
158
313
|
res;
|
|
159
314
|
|
|
160
|
-
|
|
161
|
-
|
|
315
|
+
/**
|
|
316
|
+
* Copies one header out of uWS and notices the two things the constructor decides by.
|
|
317
|
+
*
|
|
318
|
+
* One function for every request, fed through currentRequest: an arrow in the constructor
|
|
319
|
+
* captured `this`, which cost a context and a function allocation per request.
|
|
320
|
+
*
|
|
321
|
+
* @param {string} headerKey lowercase, as uWS hands it over
|
|
322
|
+
* @param {string} value
|
|
323
|
+
*/
|
|
162
324
|
static #collectHeader = (headerKey, value) => {
|
|
163
325
|
const r = currentRequest;
|
|
164
326
|
r.#rawHeadersEntries.push(headerKey, value);
|
|
@@ -173,20 +335,130 @@ module.exports = class Request extends Readable {
|
|
|
173
335
|
) {
|
|
174
336
|
r._connectionClose = true;
|
|
175
337
|
} else if (
|
|
176
|
-
|
|
177
|
-
// nothing: the stream ends empty either way, without the onData subscription
|
|
178
|
-
(headerKey.length === 14 && headerKey === "content-length" && value !== "0") ||
|
|
338
|
+
(headerKey.length === 14 && headerKey === "content-length") ||
|
|
179
339
|
(headerKey.length === 17 && headerKey === "transfer-encoding")
|
|
180
340
|
) {
|
|
181
|
-
//
|
|
182
|
-
|
|
341
|
+
// saying anything about framing at all, "0" included. A parser that can see a
|
|
342
|
+
// content-length answers about the body it describes, even an empty one: a zero length
|
|
343
|
+
// with a charset nobody can decode is a 415 in express and here, so a chain may only
|
|
344
|
+
// step over a parser when the request said nothing about a body whatsoever
|
|
345
|
+
r._hasBodyHeaders = true;
|
|
346
|
+
// content-length: 0 declares that there is nothing, which is the same as declaring
|
|
347
|
+
// nothing: the stream ends empty either way, without the onData subscription
|
|
348
|
+
if (value !== "0" || headerKey.length === 17) {
|
|
349
|
+
// noticed here so the body decision in the constructor does not build the headers object
|
|
350
|
+
r._declaresBody = true;
|
|
351
|
+
}
|
|
183
352
|
}
|
|
184
353
|
};
|
|
185
354
|
|
|
355
|
+
/**
|
|
356
|
+
* The parameters a native uWS route matched, by name, or undefined off that path.
|
|
357
|
+
* @type {Record<string, string>|undefined}
|
|
358
|
+
*/
|
|
186
359
|
optimizedParams;
|
|
187
360
|
|
|
361
|
+
/**
|
|
362
|
+
* Whether a body parser has already read this request, so a second one leaves it alone.
|
|
363
|
+
* @type {boolean|undefined}
|
|
364
|
+
*/
|
|
365
|
+
bodyRead;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The route currently running, which express hands to a handler through the request.
|
|
369
|
+
* @type {any}
|
|
370
|
+
*/
|
|
371
|
+
route;
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Which hop the error being carried came from, so an error handler declared before it does
|
|
375
|
+
* not catch what happened after it.
|
|
376
|
+
* @type {number|undefined}
|
|
377
|
+
*/
|
|
378
|
+
_errorKey;
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Which app.route() the failing route belonged to, when it belonged to one. Express builds one
|
|
382
|
+
* route out of everything hung off an app.route(), so an error handler written on it catches
|
|
383
|
+
* what its siblings raised, and nothing else does.
|
|
384
|
+
* @type {number|undefined}
|
|
385
|
+
*/
|
|
386
|
+
_errorGroup;
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* How much of _originalPath the mounts entered so far have taken. Kept as a count rather than
|
|
390
|
+
* worked out from the mount patterns, because what a mount took is what it matched, and a
|
|
391
|
+
* pattern rebuilt from the whole stack does not always match the same thing.
|
|
392
|
+
* @type {number}
|
|
393
|
+
*/
|
|
394
|
+
_consumed = 0;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* next() as the router means it: the rest of the route is skipped. res.sendFile reports its
|
|
398
|
+
* failures here, because express reports them to the router and not to the route.
|
|
399
|
+
* @type {((err?: any) => void)|undefined}
|
|
400
|
+
*/
|
|
401
|
+
_leaveRoute;
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* What `readable` answers while there is no stream to ask, see LazyReadable. Declared so the
|
|
405
|
+
* class has one shape whether or not anything ever streams.
|
|
406
|
+
* @type {boolean}
|
|
407
|
+
*/
|
|
408
|
+
_readableFlag = true;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* The peer address as uWS hands it over, sixteen bytes or four.
|
|
412
|
+
*
|
|
413
|
+
* Declared although the constructor only sometimes fills it in: a property that appears on
|
|
414
|
+
* some requests and not others gives the class more than one shape, and every read of every
|
|
415
|
+
* other field pays for that.
|
|
416
|
+
*
|
|
417
|
+
* @type {ArrayBuffer|undefined}
|
|
418
|
+
*/
|
|
419
|
+
rawIp;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Whether the request declared a body, content-length or transfer-encoding, spotted during
|
|
423
|
+
* the header copy. Declared for the same reason as rawIp.
|
|
424
|
+
* @type {boolean|undefined}
|
|
425
|
+
*/
|
|
426
|
+
_declaresBody;
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Whether the request said anything at all about framing, a content-length of "0" included.
|
|
430
|
+
* Wider than _declaresBody on purpose: a parser that can see a content-length answers about
|
|
431
|
+
* the body it describes even when that body is empty, so this is what decides whether a chain
|
|
432
|
+
* may step over one. Declared for the same reason as rawIp.
|
|
433
|
+
* @type {boolean|undefined}
|
|
434
|
+
*/
|
|
435
|
+
_hasBodyHeaders;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Whether the client asked for the connection to be closed. Declared for the same reason.
|
|
439
|
+
* @type {boolean|undefined}
|
|
440
|
+
*/
|
|
441
|
+
_connectionClose;
|
|
442
|
+
|
|
443
|
+
/**
|
|
444
|
+
* The continuation of the chain currently running, which express also hands to a handler
|
|
445
|
+
* through the request. Declared rather than left to appear on assignment: runRoute sets it
|
|
446
|
+
* on every request, and an undeclared property is a shape change on each one.
|
|
447
|
+
*
|
|
448
|
+
* @type {any}
|
|
449
|
+
*/
|
|
450
|
+
next;
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* What the chain threw or passed to next(err), waiting for an error handler.
|
|
454
|
+
* @type {any}
|
|
455
|
+
*/
|
|
188
456
|
_error;
|
|
189
457
|
|
|
458
|
+
/**
|
|
459
|
+
* Set by the paths that must not earn an ETag, res.sendFile's stream among them.
|
|
460
|
+
* @type {boolean|undefined}
|
|
461
|
+
*/
|
|
190
462
|
noEtag;
|
|
191
463
|
|
|
192
464
|
/**
|
|
@@ -204,11 +476,13 @@ module.exports = class Request extends Readable {
|
|
|
204
476
|
* literal registration, a holder of its own for a parameterised one
|
|
205
477
|
*/
|
|
206
478
|
constructor(req, res, app, preset, skipHolder) {
|
|
207
|
-
// the
|
|
208
|
-
super(
|
|
479
|
+
// nothing: the stream is built on the first touch, see LazyReadable
|
|
480
|
+
super();
|
|
209
481
|
this._res = res;
|
|
210
482
|
this._req = req;
|
|
211
|
-
|
|
483
|
+
// the plain field behind the `readable` accessor, written rather than set so a request that
|
|
484
|
+
// never streams never builds a stream
|
|
485
|
+
this._readableFlag = true;
|
|
212
486
|
if (skipHolder !== undefined && skipHolder.skipHeaders) {
|
|
213
487
|
// The chain behind this registration provably never reads a header, so instead of
|
|
214
488
|
// copying them all out of uWS the constructor asks for the four that steer the
|
|
@@ -217,6 +491,9 @@ module.exports = class Request extends Readable {
|
|
|
217
491
|
// the parsers and the stream want the whole picture, so it takes the full copy.
|
|
218
492
|
const length = req.getHeader("content-length");
|
|
219
493
|
const transferEncoding = req.getHeader("transfer-encoding");
|
|
494
|
+
if (length !== "" || transferEncoding !== "") {
|
|
495
|
+
this._hasBodyHeaders = true;
|
|
496
|
+
}
|
|
220
497
|
if ((length !== "" && length !== "0") || transferEncoding !== "") {
|
|
221
498
|
currentRequest = this;
|
|
222
499
|
this._req.forEach(Request.#collectHeader);
|
|
@@ -270,8 +547,11 @@ module.exports = class Request extends Readable {
|
|
|
270
547
|
this._rawQuery = "";
|
|
271
548
|
this.urlQuery = "";
|
|
272
549
|
} else {
|
|
273
|
-
|
|
274
|
-
|
|
550
|
+
// getQuery tells "/a" from "/a?": no query string at all reads undefined, an empty
|
|
551
|
+
// one reads "". Express keeps that lone "?" in req.url, so the two are kept apart
|
|
552
|
+
const rawQuery = req.getQuery();
|
|
553
|
+
this._rawQuery = rawQuery ?? "";
|
|
554
|
+
this.urlQuery = rawQuery === undefined ? "" : "?" + rawQuery;
|
|
275
555
|
}
|
|
276
556
|
if (preset) {
|
|
277
557
|
// the registration's constants: two native crossings and their strings not asked for
|
|
@@ -299,9 +579,6 @@ module.exports = class Request extends Readable {
|
|
|
299
579
|
this.endsWithSlash = this.path.charCodeAt(this.path.length - 1) === 0x2f;
|
|
300
580
|
this._opPath = this.path;
|
|
301
581
|
this._originalPath = this.path;
|
|
302
|
-
if (this.endsWithSlash && this.path !== "/" && !this.app.get("strict routing")) {
|
|
303
|
-
this._opPath = this._opPath.slice(0, -1);
|
|
304
|
-
}
|
|
305
582
|
this.method = req.getCaseSensitiveMethod().toUpperCase();
|
|
306
583
|
this._isOptions = this.method === "OPTIONS";
|
|
307
584
|
this._isHead = this.method === "HEAD";
|
|
@@ -322,10 +599,13 @@ module.exports = class Request extends Readable {
|
|
|
322
599
|
// null for the same reason as the two above: a request that never enters a mount never
|
|
323
600
|
// needs either array, and the push sites materialize them
|
|
324
601
|
this._stack = null;
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
this.
|
|
602
|
+
// how many characters of _originalPath the mounts entered so far have taken, which is
|
|
603
|
+
// where baseUrl ends and the path below them begins
|
|
604
|
+
this._consumed = 0;
|
|
328
605
|
this._paramStack = null;
|
|
606
|
+
// route and application in pairs, one pair per mounted application entered from another
|
|
607
|
+
// application, so handing back puts the one that was current back, see rememberApp
|
|
608
|
+
this._appStack = undefined;
|
|
329
609
|
this.receivedData = false;
|
|
330
610
|
// reading ip is very slow in UWS, so its better to not do it unless truly needed
|
|
331
611
|
if (this.app.needsIpAfterResponse || this.key < 100) {
|
|
@@ -435,8 +715,8 @@ module.exports = class Request extends Readable {
|
|
|
435
715
|
if (this._baseUrlOverride !== undefined) {
|
|
436
716
|
return this._baseUrlOverride;
|
|
437
717
|
}
|
|
438
|
-
|
|
439
|
-
return
|
|
718
|
+
// what the mounts took, which is where the path they left off begins
|
|
719
|
+
return this._consumed === 0 ? "" : this._originalPath.slice(0, this._consumed);
|
|
440
720
|
}
|
|
441
721
|
|
|
442
722
|
/**
|
|
@@ -593,13 +873,13 @@ module.exports = class Request extends Readable {
|
|
|
593
873
|
? this._originalPath
|
|
594
874
|
: this._originalPath.slice(0, this._originalPath.length - oldPath.length);
|
|
595
875
|
this._rawQuery = queryIndex === -1 ? "" : newUrl.slice(queryIndex + 1);
|
|
596
|
-
|
|
876
|
+
// a rewrite to "/a?" keeps its "?", as one arriving that way does
|
|
877
|
+
this.urlQuery = queryIndex === -1 ? "" : "?" + this._rawQuery;
|
|
597
878
|
this.#cachedQuery = null;
|
|
598
879
|
this._originalPath = prefix + newPath;
|
|
599
880
|
this.path = newPath;
|
|
600
881
|
this.endsWithSlash = newPath.charCodeAt(newPath.length - 1) === 0x2f;
|
|
601
|
-
this._opPath =
|
|
602
|
-
this.endsWithSlash && newPath !== "/" && !this.app.get("strict routing") ? newPath.slice(0, -1) : newPath;
|
|
882
|
+
this._opPath = newPath;
|
|
603
883
|
this._lastUrl = newUrl;
|
|
604
884
|
}
|
|
605
885
|
|
|
@@ -693,22 +973,34 @@ module.exports = class Request extends Readable {
|
|
|
693
973
|
}
|
|
694
974
|
this.rawIp = this._res.getRemoteAddress();
|
|
695
975
|
}
|
|
976
|
+
// read once: the branch above settled it, and every use below wants the bytes
|
|
977
|
+
const rawIp = /** @type {ArrayBuffer} */ (this.rawIp);
|
|
696
978
|
/** @type {string|undefined} */
|
|
697
979
|
let ip;
|
|
698
|
-
if (
|
|
980
|
+
if (rawIp.byteLength === 4) {
|
|
699
981
|
// ipv4
|
|
700
|
-
ip = new Uint8Array(
|
|
982
|
+
ip = new Uint8Array(rawIp).join(".");
|
|
701
983
|
if (mapsIPv4Peer(this.app)) {
|
|
702
984
|
ip = "::ffff:" + ip;
|
|
703
985
|
}
|
|
704
|
-
} else if (
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
groups
|
|
986
|
+
} else if (rawIp.byteLength === 16) {
|
|
987
|
+
const bytes = new Uint8Array(rawIp);
|
|
988
|
+
if (isMappedIPv4(bytes)) {
|
|
989
|
+
// ::ffff:a.b.c.d, which is what a dual stack listener hands over for every IPv4
|
|
990
|
+
// peer, so it is what nearly every request here is. The general path below reaches
|
|
991
|
+
// the same string through a DataView, an array of eight groups and a scan for the
|
|
992
|
+
// longest run of zeros, and measured 157ns more per request for it. Anything that
|
|
993
|
+
// reads req.ip pays that once, and morgan reads it on every line it writes.
|
|
994
|
+
ip = "::ffff:" + bytes[12] + "." + bytes[13] + "." + bytes[14] + "." + bytes[15];
|
|
995
|
+
} else {
|
|
996
|
+
// ipv6
|
|
997
|
+
const dv = new DataView(rawIp);
|
|
998
|
+
const groups = new Array(8);
|
|
999
|
+
for (let i = 0; i < 8; i++) {
|
|
1000
|
+
groups[i] = dv.getUint16(i * 2);
|
|
1001
|
+
}
|
|
1002
|
+
ip = formatIPv6(groups);
|
|
710
1003
|
}
|
|
711
|
-
ip = formatIPv6(groups);
|
|
712
1004
|
} else {
|
|
713
1005
|
ip = undefined; // unix sockets dont have ip
|
|
714
1006
|
}
|
|
@@ -749,6 +1041,33 @@ module.exports = class Request extends Readable {
|
|
|
749
1041
|
return this.connection;
|
|
750
1042
|
}
|
|
751
1043
|
|
|
1044
|
+
/**
|
|
1045
|
+
* Cuts this request loose from the µWS response it arrived on, keeping the two things only
|
|
1046
|
+
* that response could answer.
|
|
1047
|
+
*
|
|
1048
|
+
* A websocket upgrade hands the request to the socket, which outlives the response by the
|
|
1049
|
+
* whole life of the connection. Reading the peer address through the freed response is not
|
|
1050
|
+
* an error but a use after free, so the values are taken while it is still alive and an
|
|
1051
|
+
* inert stand-in answers anything that asks later.
|
|
1052
|
+
*/
|
|
1053
|
+
_detachFromResponse() {
|
|
1054
|
+
const uwsRes = this._res;
|
|
1055
|
+
if (!this.rawIp) {
|
|
1056
|
+
this.rawIp = uwsRes.getRemoteAddress();
|
|
1057
|
+
}
|
|
1058
|
+
const remotePort = uwsRes.getRemotePort();
|
|
1059
|
+
const rawIp = this.rawIp;
|
|
1060
|
+
this._res = {
|
|
1061
|
+
getRemoteAddress: () => rawIp,
|
|
1062
|
+
getRemotePort: () => remotePort,
|
|
1063
|
+
// a body cannot arrive on an upgraded socket, and a stray reader must not reach µWS
|
|
1064
|
+
onData() {},
|
|
1065
|
+
pause() {},
|
|
1066
|
+
resume() {},
|
|
1067
|
+
close() {}
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
|
|
752
1071
|
/**
|
|
753
1072
|
* Whether the client's cached copy is still good, from If-None-Match and If-Modified-Since
|
|
754
1073
|
* against the response headers set so far. Only GET and HEAD can be fresh.
|