fulmine.js 5.0.0-rc.1
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/EXPRESS_LICENSE +26 -0
- package/LICENSE +202 -0
- package/NOTICE +38 -0
- package/README.md +469 -0
- package/package.json +165 -0
- package/src/application.js +561 -0
- package/src/cli.js +369 -0
- package/src/declarative.js +768 -0
- package/src/index.js +71 -0
- package/src/middlewares.js +636 -0
- package/src/node-shim.js +400 -0
- package/src/request.js +807 -0
- package/src/response.js +1360 -0
- package/src/router.js +1240 -0
- package/src/types.d.ts +62 -0
- package/src/utils.js +993 -0
- package/src/view.js +172 -0
- package/src/worker.js +38 -0
package/src/utils.js
ADDED
|
@@ -0,0 +1,993 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2024 dimden.dev
|
|
3
|
+
Copyright 2026 Nigro Simone
|
|
4
|
+
|
|
5
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
you may not use this file except in compliance with the License.
|
|
7
|
+
You may obtain a copy of the License at
|
|
8
|
+
|
|
9
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
|
|
11
|
+
Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
See the License for the specific language governing permissions and
|
|
15
|
+
limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const mime = require("mime-types");
|
|
19
|
+
const path = require("path");
|
|
20
|
+
const proxyaddr = require("proxy-addr");
|
|
21
|
+
const qs = require("qs");
|
|
22
|
+
const querystring = require("fast-querystring");
|
|
23
|
+
const crypto = require("crypto");
|
|
24
|
+
const statuses = require("statuses");
|
|
25
|
+
const { Stats } = require("fs");
|
|
26
|
+
|
|
27
|
+
const EMPTY_REGEX = new RegExp(``);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Parses a query string the way the "extended" parser is expected to, which means qs and its
|
|
31
|
+
* support for nested keys, but without paying for qs on the strings that cannot need it. A short
|
|
32
|
+
* query with no bracket and no dot goes through fast-querystring instead, which is several times
|
|
33
|
+
* quicker and produces the same answer for that shape.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} query the query string, without the leading "?"
|
|
36
|
+
* @param {object} [options] passed through to qs when it is used
|
|
37
|
+
* @returns {Record<string, any>} null-prototype, so a key from the query cannot reach Object.prototype
|
|
38
|
+
*/
|
|
39
|
+
function fastQueryParse(query, options) {
|
|
40
|
+
// the result keeps a null prototype, which is why req.query prints as "[Object: null
|
|
41
|
+
// prototype] {}". Spreading it into a plain object would put Object.prototype keys back
|
|
42
|
+
// within reach of a query string.
|
|
43
|
+
const len = query.length;
|
|
44
|
+
if (len === 0) {
|
|
45
|
+
return Object.create(null);
|
|
46
|
+
}
|
|
47
|
+
if (len <= 128) {
|
|
48
|
+
if (!query.includes("[") && !query.includes("%5B") && !query.includes(".") && !query.includes("%2E")) {
|
|
49
|
+
return Object.assign(Object.create(null), querystring.parse(query));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return Object.assign(Object.create(null), qs.parse(query, options));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Collapses runs of slashes, so //a///b reads as /a/b. Express does the same before matching, and
|
|
57
|
+
* without it a path could dodge a route by being written with an extra slash.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} path
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function removeDuplicateSlashes(path) {
|
|
63
|
+
return path.replace(/\/{2,}/g, "/");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// the opening of a named capture group, which is how the parameter names are read back out of a
|
|
67
|
+
// finished pattern
|
|
68
|
+
const NAMED_GROUP = /\(\?<([^>]+)>/g;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* What a compiled pattern captures: the names in order, and which of them are wildcards to split
|
|
72
|
+
* into an array. Here and not on the regex itself, because one own property takes a RegExp off V8's
|
|
73
|
+
* fast path: replace() went from 37ns to 808ns and test() from 22ns to 78ns.
|
|
74
|
+
*
|
|
75
|
+
* @typedef {{wildcardNames: string[], paramNames: string[], isWildcard: boolean[]}} PatternMeta
|
|
76
|
+
*/
|
|
77
|
+
const patternMeta = new WeakMap();
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What patternToRegex worked out about a pattern, or undefined for a RegExp the application wrote
|
|
81
|
+
* itself.
|
|
82
|
+
* @param {RegExp} pattern
|
|
83
|
+
* @returns {PatternMeta|undefined}
|
|
84
|
+
*/
|
|
85
|
+
function getPatternMeta(pattern) {
|
|
86
|
+
return patternMeta.get(pattern);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A compiled path pattern. A plain RegExp, deliberately.
|
|
91
|
+
* @typedef {RegExp} PathRegExp
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Compiles a path into a regex, following path-to-regexp v8:
|
|
96
|
+
* - :param a named parameter, one segment
|
|
97
|
+
* - /*splat a named wildcard, one or more segments, captured as an array
|
|
98
|
+
* - {...} an optional group
|
|
99
|
+
* - \x an escaped literal
|
|
100
|
+
*
|
|
101
|
+
* A bare `*`, an unnamed parameter, an inline regex like :id(\\d+) and the `+`, `?`, `()` operators
|
|
102
|
+
* throw: a route that quietly stops matching is worse than one that fails at startup. The names it
|
|
103
|
+
* captures go in a WeakMap beside the regex, see PatternMeta.
|
|
104
|
+
*/
|
|
105
|
+
function patternToRegex(pattern, isPrefix = false) {
|
|
106
|
+
if (pattern instanceof RegExp) {
|
|
107
|
+
return pattern;
|
|
108
|
+
}
|
|
109
|
+
if (isPrefix && pattern === "") {
|
|
110
|
+
return EMPTY_REGEX;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let regexPattern = "";
|
|
114
|
+
let i = 0;
|
|
115
|
+
const len = pattern.length;
|
|
116
|
+
const wildcardNames = [];
|
|
117
|
+
// whether the token just emitted was a :parameter, which decides how greedy the next
|
|
118
|
+
// optional group is allowed to be. see the comment where it is read
|
|
119
|
+
let lastTokenWasParam = false;
|
|
120
|
+
|
|
121
|
+
while (i < len) {
|
|
122
|
+
const ch = pattern[i];
|
|
123
|
+
|
|
124
|
+
if (ch === "\\" && i + 1 < len) {
|
|
125
|
+
regexPattern += "\\" + pattern[i + 1];
|
|
126
|
+
i += 2;
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// *splat: one or more characters, slashes included. It is not anchored to a segment
|
|
131
|
+
// boundary, so /te*st is literal "/te" followed by a wildcard named "st"
|
|
132
|
+
if (ch === "*") {
|
|
133
|
+
const at = i;
|
|
134
|
+
i++;
|
|
135
|
+
let name = "";
|
|
136
|
+
while (i < len && /\w/.test(pattern[i])) {
|
|
137
|
+
name += pattern[i++];
|
|
138
|
+
}
|
|
139
|
+
if (!name) {
|
|
140
|
+
throw new Error(`Missing parameter name at index ${at}: ${pattern}`);
|
|
141
|
+
}
|
|
142
|
+
wildcardNames.push(name);
|
|
143
|
+
regexPattern += `(?<${name}>[^]+)`;
|
|
144
|
+
lastTokenWasParam = false;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (ch === "{") {
|
|
149
|
+
// {*splat}: zero or more segments, so it also matches the mount point
|
|
150
|
+
if (pattern[i + 1] === "*") {
|
|
151
|
+
i += 2;
|
|
152
|
+
let name = "";
|
|
153
|
+
while (i < len && pattern[i] !== "}") {
|
|
154
|
+
name += pattern[i++];
|
|
155
|
+
}
|
|
156
|
+
i++;
|
|
157
|
+
if (!name) {
|
|
158
|
+
throw new Error(`Wildcard must be named in Express 5: use {*splat} (in "${pattern}")`);
|
|
159
|
+
}
|
|
160
|
+
wildcardNames.push(name);
|
|
161
|
+
if (regexPattern.endsWith("/") || regexPattern.endsWith("\\/")) {
|
|
162
|
+
// the slash belongs to the optional part, otherwise /{*splat} would not match /
|
|
163
|
+
regexPattern = regexPattern.slice(0, regexPattern.endsWith("\\/") ? -2 : -1);
|
|
164
|
+
regexPattern += `(?:/(?<${name}>.+))?/?`;
|
|
165
|
+
} else {
|
|
166
|
+
regexPattern += `(?<${name}>.*)`;
|
|
167
|
+
}
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// optional group, which may itself contain a parameter: {.:ext}, {/:page}
|
|
172
|
+
i++;
|
|
173
|
+
let groupContent = "";
|
|
174
|
+
let braceDepth = 1;
|
|
175
|
+
while (i < len && braceDepth > 0) {
|
|
176
|
+
if (pattern[i] === "{") braceDepth++;
|
|
177
|
+
else if (pattern[i] === "}") {
|
|
178
|
+
braceDepth--;
|
|
179
|
+
if (braceDepth === 0) break;
|
|
180
|
+
}
|
|
181
|
+
groupContent += pattern[i++];
|
|
182
|
+
}
|
|
183
|
+
i++;
|
|
184
|
+
|
|
185
|
+
// When a :parameter precedes this group, that parameter is the one that gives ground
|
|
186
|
+
// while backtracking, so this one must not swallow the separator as well. Express
|
|
187
|
+
// splits /a.b.c against /:file{.:ext} as file=a.b, ext=c, which only works if ext
|
|
188
|
+
// cannot contain a dot. After static text there is nothing to give ground, so the
|
|
189
|
+
// parameter takes everything: /file{.:ext} against /file.tar.gz gives ext=tar.gz.
|
|
190
|
+
const separator = lastTokenWasParam && groupContent[0] && groupContent[0] !== ":" ? groupContent[0] : "";
|
|
191
|
+
const groupParamClass = `[^/${separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}]+`;
|
|
192
|
+
|
|
193
|
+
let groupRegex = "";
|
|
194
|
+
let gi = 0;
|
|
195
|
+
while (gi < groupContent.length) {
|
|
196
|
+
if (groupContent[gi] === ":") {
|
|
197
|
+
gi++;
|
|
198
|
+
let paramName = "";
|
|
199
|
+
while (gi < groupContent.length && /\w/.test(groupContent[gi])) {
|
|
200
|
+
paramName += groupContent[gi++];
|
|
201
|
+
}
|
|
202
|
+
groupRegex += `(?<${paramName}>${groupParamClass})`;
|
|
203
|
+
} else if (groupContent[gi] === ".") {
|
|
204
|
+
groupRegex += "\\.";
|
|
205
|
+
gi++;
|
|
206
|
+
} else if (groupContent[gi] === "/") {
|
|
207
|
+
groupRegex += "/";
|
|
208
|
+
gi++;
|
|
209
|
+
} else {
|
|
210
|
+
groupRegex += groupContent[gi].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
211
|
+
gi++;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
regexPattern += `(?:${groupRegex})?`;
|
|
215
|
+
lastTokenWasParam = false;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (ch === ":") {
|
|
220
|
+
i++;
|
|
221
|
+
let name = "";
|
|
222
|
+
while (i < len && /\w/.test(pattern[i])) {
|
|
223
|
+
name += pattern[i++];
|
|
224
|
+
}
|
|
225
|
+
if (!name) {
|
|
226
|
+
throw new Error(`Missing parameter name at index ${i - 1}: ${pattern}`);
|
|
227
|
+
}
|
|
228
|
+
// a following optional group needs room to match, so the parameter gives ground
|
|
229
|
+
regexPattern += i < len && pattern[i] === "{" ? `(?<${name}>[^/]+?)` : `(?<${name}>[^/]+)`;
|
|
230
|
+
lastTokenWasParam = true;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// these have no meaning in a path, and the route is refused rather than matching them
|
|
235
|
+
// literally: a path that quietly stops matching is worse than one that fails at startup.
|
|
236
|
+
// Escape them to use them as literals.
|
|
237
|
+
if ("?+()[]!".includes(ch)) {
|
|
238
|
+
throw new Error(`Unexpected ${ch} at index ${i}: ${pattern}`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (".^$|}".includes(ch)) {
|
|
242
|
+
regexPattern += "\\" + ch;
|
|
243
|
+
} else {
|
|
244
|
+
regexPattern += ch;
|
|
245
|
+
}
|
|
246
|
+
lastTokenWasParam = false;
|
|
247
|
+
i++;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const regex = /** @type {PathRegExp} */ (new RegExp(`^${regexPattern}${isPrefix ? "(?=$|/)" : "$"}`));
|
|
251
|
+
// read back out of the finished pattern, so the list cannot disagree with the regex. Asking for
|
|
252
|
+
// each name in turn beats walking match.groups with for-in: 176ns against 349
|
|
253
|
+
const paramNames = [...regexPattern.matchAll(NAMED_GROUP)].map((m) => m[1]);
|
|
254
|
+
patternMeta.set(regex, {
|
|
255
|
+
wildcardNames,
|
|
256
|
+
paramNames,
|
|
257
|
+
isWildcard: paramNames.map((name) => wildcardNames.includes(name))
|
|
258
|
+
});
|
|
259
|
+
return regex;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Whether a path has anything in it that a string comparison cannot answer, which is a parameter,
|
|
264
|
+
* a wildcard or an optional group. A regular expression is already compiled, so it needs nothing.
|
|
265
|
+
*
|
|
266
|
+
* @param {string|RegExp} pattern
|
|
267
|
+
* @returns {boolean}
|
|
268
|
+
*/
|
|
269
|
+
function needsConversionToRegex(pattern) {
|
|
270
|
+
if (pattern instanceof RegExp) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return pattern.includes("*") || pattern.includes(":") || pattern.includes("{");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Whether a path is a plain literal, which is what makes a route eligible for the native uWS
|
|
279
|
+
* router. Not simply the opposite of needsConversionToRegex: a RegExp answers false to both, since
|
|
280
|
+
* it needs no conversion and cannot be optimized either.
|
|
281
|
+
*
|
|
282
|
+
* @param {string|RegExp} pattern
|
|
283
|
+
* @returns {boolean}
|
|
284
|
+
*/
|
|
285
|
+
function canBeOptimized(pattern) {
|
|
286
|
+
if (pattern instanceof RegExp) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
return !pattern.includes("*") && !pattern.includes("{") && !pattern.includes(":");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// a parameter that is the whole segment, which is the only shape µWS matches the same way Express
|
|
293
|
+
// does. "/flights/:from-:to" is one segment to µWS and two parameters to Express.
|
|
294
|
+
const WHOLE_SEGMENT_PARAM = /^:\w+$/;
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Whether µWS's own router matches this path exactly as Express would, parameters included.
|
|
298
|
+
*
|
|
299
|
+
* µWS matches `:name` against one non-empty segment, as Express does, and has nothing for a v5
|
|
300
|
+
* wildcard or an optional group. A parameter counts only when it is the whole segment: Express
|
|
301
|
+
* matches `/flights/:from-:to` inside a segment and µWS does not.
|
|
302
|
+
*
|
|
303
|
+
* @param {string|RegExp} pattern
|
|
304
|
+
* @returns {boolean}
|
|
305
|
+
*/
|
|
306
|
+
function canBeOptimizedWithParams(pattern) {
|
|
307
|
+
if (pattern instanceof RegExp) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
if (/[*{}()[\]?+\\]/.test(pattern)) {
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
if (!pattern.includes(":")) {
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
for (const segment of pattern.split("/")) {
|
|
317
|
+
if (segment.includes(":") && !WHOLE_SEGMENT_PARAM.test(segment)) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Whether two paths could both match the same request.
|
|
326
|
+
*
|
|
327
|
+
* Only asked about paths µWS could match itself, so the answer is structural: the same number of
|
|
328
|
+
* segments, and no position where two different literals meet. `/orders/:id` and `/invoices/:id`
|
|
329
|
+
* cannot both match, `/users/:id` and `/users/me` can. Anything else is not asked, and the caller
|
|
330
|
+
* reads "do not know" as yes.
|
|
331
|
+
*
|
|
332
|
+
* @param {string} a
|
|
333
|
+
* @param {string} b
|
|
334
|
+
* @returns {boolean}
|
|
335
|
+
*/
|
|
336
|
+
function pathsCanOverlap(a, b) {
|
|
337
|
+
const left = a.split("/");
|
|
338
|
+
const right = b.split("/");
|
|
339
|
+
if (left.length !== right.length) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
for (let i = 0; i < left.length; i++) {
|
|
343
|
+
if (left[i] === right[i]) {
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
// a parameter matches whatever is in that segment, so only two different literals settle it
|
|
347
|
+
if (left[i].charCodeAt(0) === 0x3a || right[i].charCodeAt(0) === 0x3a) {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Splits one entry of an Accept-style header into its value and its parameters, with q pulled out
|
|
357
|
+
* as the quality since that is the one every caller wants.
|
|
358
|
+
*
|
|
359
|
+
* @param {string} str a single entry, such as "text/html;q=0.8;level=1"
|
|
360
|
+
* @returns {{value: string, quality: number, params: Record<string, string>}}
|
|
361
|
+
*/
|
|
362
|
+
function acceptParams(str) {
|
|
363
|
+
const length = str.length;
|
|
364
|
+
const colonIndex = str.indexOf(";");
|
|
365
|
+
let index = colonIndex === -1 ? length : colonIndex;
|
|
366
|
+
const ret = { value: str.slice(0, index).trim(), quality: 1, params: {} };
|
|
367
|
+
|
|
368
|
+
while (index < length) {
|
|
369
|
+
const splitIndex = str.indexOf("=", index);
|
|
370
|
+
if (splitIndex === -1) break;
|
|
371
|
+
|
|
372
|
+
const colonIndex = str.indexOf(";", index);
|
|
373
|
+
const endIndex = colonIndex === -1 ? length : colonIndex;
|
|
374
|
+
|
|
375
|
+
if (splitIndex > endIndex) {
|
|
376
|
+
index = str.lastIndexOf(";", splitIndex - 1) + 1;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const key = str.slice(index, splitIndex).trim();
|
|
381
|
+
const value = str.slice(splitIndex + 1, endIndex).trim();
|
|
382
|
+
|
|
383
|
+
if (key === "q") {
|
|
384
|
+
ret.quality = parseFloat(value);
|
|
385
|
+
} else {
|
|
386
|
+
ret.params[key] = value;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
index = endIndex + 1;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return ret;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// How many answers a memo keeps before it starts over.
|
|
396
|
+
//
|
|
397
|
+
// The keys are media types, so an application uses a handful and the ceiling is never approached.
|
|
398
|
+
// It is here because application code is free to hand res.type() something a client sent, and an
|
|
399
|
+
// unbounded map keyed on that is a leak the client controls.
|
|
400
|
+
//
|
|
401
|
+
// Clearing beats evicting one entry at a time: the few types an application really uses are back
|
|
402
|
+
// within a few requests, whereas refusing new entries once full would let a flood of invented
|
|
403
|
+
// values lock the real ones out for the life of the process.
|
|
404
|
+
const MEMO_LIMIT = 512;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* A pure function of one string, with its answers kept.
|
|
408
|
+
*
|
|
409
|
+
* The wrapped function must never answer undefined, since that is what the cache reads as a miss.
|
|
410
|
+
* The mime lookups here answer false for something they do not know, which caches correctly.
|
|
411
|
+
*
|
|
412
|
+
* @param {(key: string) => any} fn
|
|
413
|
+
* @returns {(key: string) => any}
|
|
414
|
+
*/
|
|
415
|
+
function memoizeByString(fn) {
|
|
416
|
+
const cache = new Map();
|
|
417
|
+
return function memoized(key) {
|
|
418
|
+
let hit = cache.get(key);
|
|
419
|
+
if (hit === undefined) {
|
|
420
|
+
hit = fn(key);
|
|
421
|
+
if (cache.size >= MEMO_LIMIT) {
|
|
422
|
+
cache.clear();
|
|
423
|
+
}
|
|
424
|
+
cache.set(key, hit);
|
|
425
|
+
}
|
|
426
|
+
return hit;
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// mime.lookup walks the extension and searches the database for it, which for the same "json" on
|
|
431
|
+
// every response is 273 ns to reach the same answer. Kept, it is 6.
|
|
432
|
+
const lookupType = memoizeByString((type) => mime.lookup(type) || "application/octet-stream");
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* The full content-type an extension stands for, charset included, as res.type() writes it.
|
|
436
|
+
* @param {string} type an extension, or a media type, which is returned as given
|
|
437
|
+
* @returns {string}
|
|
438
|
+
*/
|
|
439
|
+
const contentTypeFor = memoizeByString((type) => mime.contentType(type) || "application/octet-stream");
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* A media type from either spelling: "html" is looked up in the mime database, while anything
|
|
443
|
+
* containing a slash is already one and is parsed for its parameters.
|
|
444
|
+
*
|
|
445
|
+
* @param {string} type an extension or a full media type
|
|
446
|
+
* @returns {{value: string, params: Record<string, string>}}
|
|
447
|
+
*/
|
|
448
|
+
function normalizeType(type) {
|
|
449
|
+
// a fresh object every time on purpose: the caller owns params and may write to it
|
|
450
|
+
return ~type.indexOf("/") ? acceptParams(type) : { value: lookupType(type), params: {} };
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* JSON.stringify, plus the escaping the "json escape" setting asks for: <, > and & become their
|
|
455
|
+
* unicode escapes, so a string in the body cannot close a script tag in an HTML page that embeds
|
|
456
|
+
* the response.
|
|
457
|
+
*
|
|
458
|
+
* @param {any} value
|
|
459
|
+
* @param {any} [replacer] the "json replacer" setting
|
|
460
|
+
* @param {string|number} [spaces] the "json spaces" setting
|
|
461
|
+
* @param {boolean} [escape] the "json escape" setting
|
|
462
|
+
* @returns {string}
|
|
463
|
+
*/
|
|
464
|
+
function stringify(value, replacer, spaces, escape) {
|
|
465
|
+
let json = replacer || spaces ? JSON.stringify(value, replacer, spaces) : JSON.stringify(value);
|
|
466
|
+
|
|
467
|
+
if (escape && typeof json === "string") {
|
|
468
|
+
json = json.replace(/[<>&]/g, function (c) {
|
|
469
|
+
switch (c.charCodeAt(0)) {
|
|
470
|
+
case 0x3c:
|
|
471
|
+
return "\\u003c";
|
|
472
|
+
case 0x3e:
|
|
473
|
+
return "\\u003e";
|
|
474
|
+
case 0x26:
|
|
475
|
+
return "\\u0026";
|
|
476
|
+
default:
|
|
477
|
+
return c;
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return json;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const defaultSettings = {
|
|
486
|
+
"jsonp callback name": "callback",
|
|
487
|
+
env: () => process.env.NODE_ENV ?? "development",
|
|
488
|
+
etag: "weak",
|
|
489
|
+
"etag fn": () => createETagGenerator({ weak: true }),
|
|
490
|
+
"query parser": "simple",
|
|
491
|
+
"query parser fn": () => querystring.parse,
|
|
492
|
+
"subdomain offset": 2,
|
|
493
|
+
"trust proxy": false,
|
|
494
|
+
views: () => path.join(process.cwd(), "views"),
|
|
495
|
+
"view cache": () => process.env.NODE_ENV === "production",
|
|
496
|
+
// off by default, unlike Express, which keeps it for historical reasons. It only tells anyone
|
|
497
|
+
// asking which framework is running, and every hardening guide says to remove it. Set it back
|
|
498
|
+
// to true if something depends on it.
|
|
499
|
+
"x-powered-by": false,
|
|
500
|
+
"case sensitive routing": true,
|
|
501
|
+
"declarative responses": true
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Turns whatever "trust proxy" was set to into the function proxy-addr wants: a predicate saying
|
|
506
|
+
* whether the address at hop i is trusted. true trusts everything, a number trusts that many hops,
|
|
507
|
+
* and a string or a list is read as addresses and subnet names.
|
|
508
|
+
*
|
|
509
|
+
* @param {boolean|number|string|string[]|Function} val
|
|
510
|
+
* @returns {Function}
|
|
511
|
+
*/
|
|
512
|
+
function compileTrust(val) {
|
|
513
|
+
if (typeof val === "function") return val;
|
|
514
|
+
|
|
515
|
+
if (val === true) {
|
|
516
|
+
// Support plain true/false
|
|
517
|
+
return function () {
|
|
518
|
+
return true;
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (typeof val === "number") {
|
|
523
|
+
// Support trusting hop count
|
|
524
|
+
return function (a, i) {
|
|
525
|
+
return i < val;
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
if (typeof val === "string") {
|
|
530
|
+
// Support comma-separated values
|
|
531
|
+
val = val.split(",").map(function (v) {
|
|
532
|
+
return v.trim();
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return proxyaddr.compile(val || []);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const shownWarnings = new Set();
|
|
540
|
+
/**
|
|
541
|
+
* Warns once per call site that a method has a newer name, in the format the deprecate package
|
|
542
|
+
* uses, so the output sits alongside the warnings Express's own dependencies produce. Once per
|
|
543
|
+
* site rather than once per call: the same line warning on every request would be a flood.
|
|
544
|
+
*
|
|
545
|
+
* @param {string} oldMethod
|
|
546
|
+
* @param {string} newMethod
|
|
547
|
+
* @param {boolean} [full] print the whole stack rather than the one frame that called it
|
|
548
|
+
*/
|
|
549
|
+
function deprecated(oldMethod, newMethod, full = false) {
|
|
550
|
+
const err = new Error();
|
|
551
|
+
// V8 always fills this in for an Error made right here
|
|
552
|
+
const stack = err.stack ?? "";
|
|
553
|
+
const pos = full
|
|
554
|
+
? stack.split("\n").slice(1).join("\n")
|
|
555
|
+
: stack.split("\n")[3].trim().split("(").slice(1).join("(").split(")").slice(0, -1).join(")");
|
|
556
|
+
if (shownWarnings.has(pos)) return;
|
|
557
|
+
shownWarnings.add(pos);
|
|
558
|
+
console.warn(
|
|
559
|
+
`${new Date().toLocaleString("en-UK", {
|
|
560
|
+
weekday: "short",
|
|
561
|
+
year: "numeric",
|
|
562
|
+
month: "short",
|
|
563
|
+
day: "numeric",
|
|
564
|
+
hour: "numeric",
|
|
565
|
+
minute: "numeric",
|
|
566
|
+
second: "numeric",
|
|
567
|
+
timeZone: "GMT",
|
|
568
|
+
timeZoneName: "short"
|
|
569
|
+
})} fulmine.js deprecated ${oldMethod}: Use ${newMethod} instead at ${pos}`
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* findIndex, but resuming from a position. The router walks the same route list many times per
|
|
575
|
+
* request, each time picking up after the route it just ran, and Array.findIndex has no way to
|
|
576
|
+
* start anywhere but the beginning.
|
|
577
|
+
*
|
|
578
|
+
* @param {any[]} arr
|
|
579
|
+
* @param {(item: any, index: number, arr: any[]) => boolean} fn
|
|
580
|
+
* @param {number} [index] where to start
|
|
581
|
+
* @returns {number} the index, or -1
|
|
582
|
+
*/
|
|
583
|
+
function findIndexStartingFrom(arr, fn, index = 0) {
|
|
584
|
+
for (let i = index, end = arr.length; i < end; i++) {
|
|
585
|
+
if (fn(arr[i], i, arr)) {
|
|
586
|
+
return i;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return -1;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* decodeURIComponent that answers rather than throwing. A malformed escape in a URL is a bad
|
|
594
|
+
* request and not an exception, so callers check for the sentinel and answer 400.
|
|
595
|
+
*
|
|
596
|
+
* @param {string} path
|
|
597
|
+
* @returns {string|-1} -1 when the path cannot be decoded
|
|
598
|
+
*/
|
|
599
|
+
function decode(path) {
|
|
600
|
+
try {
|
|
601
|
+
return decodeURIComponent(path);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
return -1;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* A route parameter as the application should see it, which means decoded: `/users/caff%C3%A8`
|
|
609
|
+
* arrives as "caffè". A percent sequence that will not decode is the client's mistake and becomes a
|
|
610
|
+
* 400, with the message Express uses.
|
|
611
|
+
*
|
|
612
|
+
* @param {string} value
|
|
613
|
+
* @returns {string}
|
|
614
|
+
* @throws {any} carrying status 400 when the value cannot be decoded
|
|
615
|
+
*/
|
|
616
|
+
function decodeParam(value) {
|
|
617
|
+
// the common case, and worth the check: a parameter is usually a number or a word, and
|
|
618
|
+
// decodeURIComponent is not free
|
|
619
|
+
if (value.indexOf("%") === -1) {
|
|
620
|
+
return value;
|
|
621
|
+
}
|
|
622
|
+
try {
|
|
623
|
+
return decodeURIComponent(value);
|
|
624
|
+
} catch {
|
|
625
|
+
const err = /** @type {any} */ (new Error(`Failed to decode param '${value}'`));
|
|
626
|
+
err.status = 400;
|
|
627
|
+
err.statusCode = 400;
|
|
628
|
+
err.expose = true;
|
|
629
|
+
throw err;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const UP_PATH_REGEXP = /(?:^|[\\/])\.\.(?:[\\/]|$)/;
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Whether any segment is a dotfile. A single "." is not one, being the current directory, which is
|
|
637
|
+
* why the length is checked before the first character.
|
|
638
|
+
*
|
|
639
|
+
* @param {string[]} parts the path split on slashes
|
|
640
|
+
* @returns {boolean}
|
|
641
|
+
*/
|
|
642
|
+
function containsDotFile(parts) {
|
|
643
|
+
for (let i = 0, len = parts.length; i < len; i++) {
|
|
644
|
+
const part = parts[i];
|
|
645
|
+
if (part.length > 1 && part[0] === ".") {
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return false;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Splits a comma-separated header value into its tokens, trimming the spaces around each. Written
|
|
655
|
+
* out by hand rather than with split and trim because it runs for every conditional request.
|
|
656
|
+
*
|
|
657
|
+
* @param {string} str
|
|
658
|
+
* @returns {string[]}
|
|
659
|
+
*/
|
|
660
|
+
function parseTokenList(str) {
|
|
661
|
+
let end = 0;
|
|
662
|
+
const list = [];
|
|
663
|
+
let start = 0;
|
|
664
|
+
|
|
665
|
+
// gather tokens
|
|
666
|
+
for (let i = 0, len = str.length; i < len; i++) {
|
|
667
|
+
switch (str.charCodeAt(i)) {
|
|
668
|
+
case 0x20 /* */:
|
|
669
|
+
if (start === end) {
|
|
670
|
+
start = end = i + 1;
|
|
671
|
+
}
|
|
672
|
+
break;
|
|
673
|
+
case 0x2c /* , */:
|
|
674
|
+
if (start !== end) {
|
|
675
|
+
list.push(str.substring(start, end));
|
|
676
|
+
}
|
|
677
|
+
start = end = i + 1;
|
|
678
|
+
break;
|
|
679
|
+
default:
|
|
680
|
+
end = i + 1;
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// final token
|
|
686
|
+
if (start !== end) {
|
|
687
|
+
list.push(str.substring(start, end));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return list;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* An HTTP date as a timestamp, or NaN when it is missing or unreadable. NaN rather than a throw
|
|
695
|
+
* because every comparison against it is false, which is the answer a bad date should give.
|
|
696
|
+
*
|
|
697
|
+
* @param {string|undefined} date
|
|
698
|
+
* @returns {number}
|
|
699
|
+
*/
|
|
700
|
+
function parseHttpDate(date) {
|
|
701
|
+
const timestamp = date && Date.parse(date);
|
|
702
|
+
return typeof timestamp === "number" ? timestamp : NaN;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Whether the request's If-Match or If-Unmodified-Since says the copy the client is acting on is
|
|
707
|
+
* no longer the current one, which is a 412 rather than a 304: the client asked to be stopped if
|
|
708
|
+
* anything had changed.
|
|
709
|
+
*
|
|
710
|
+
* @param {any} req
|
|
711
|
+
* @param {any} res
|
|
712
|
+
* @returns {boolean}
|
|
713
|
+
*/
|
|
714
|
+
function isPreconditionFailure(req, res) {
|
|
715
|
+
const match = req.headers["if-match"];
|
|
716
|
+
|
|
717
|
+
// if-match
|
|
718
|
+
if (match) {
|
|
719
|
+
const etag = res.get("etag");
|
|
720
|
+
return (
|
|
721
|
+
!etag ||
|
|
722
|
+
(match !== "*" &&
|
|
723
|
+
parseTokenList(match).every((match) => {
|
|
724
|
+
return match !== etag && match !== "W/" + etag && "W/" + match !== etag;
|
|
725
|
+
}))
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
// if-unmodified-since
|
|
730
|
+
const unmodifiedSince = parseHttpDate(req.headers["if-unmodified-since"]);
|
|
731
|
+
if (!isNaN(unmodifiedSince)) {
|
|
732
|
+
const lastModified = parseHttpDate(res.get("Last-Modified"));
|
|
733
|
+
return isNaN(lastModified) || lastModified > unmodifiedSince;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
return false;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// the sha1 of nothing, which the etag package answers with without hashing
|
|
740
|
+
const EMPTY_ENTITY_TAG = '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
|
|
741
|
+
|
|
742
|
+
/**
|
|
743
|
+
* The ETag of a body: its length in hex, a dash, and the first 27 characters of the base64 sha1.
|
|
744
|
+
* The same string the etag package produces, and tests/unit/utils.test.js holds it to that.
|
|
745
|
+
*
|
|
746
|
+
* crypto.hash and not crypto.createHash: the one-shot form allocates no hash object, and on a 500
|
|
747
|
+
* byte body it is twice as fast for the same answer, 924ns against 1963.
|
|
748
|
+
*
|
|
749
|
+
* @param {Buffer|string} entity
|
|
750
|
+
* @param {boolean} weak
|
|
751
|
+
* @returns {string}
|
|
752
|
+
*/
|
|
753
|
+
function entityTag(entity, weak) {
|
|
754
|
+
if (entity.length === 0) {
|
|
755
|
+
return weak ? "W/" + EMPTY_ENTITY_TAG : EMPTY_ENTITY_TAG;
|
|
756
|
+
}
|
|
757
|
+
// the byte length, which for a string is not its character count
|
|
758
|
+
const len = typeof entity === "string" ? Buffer.byteLength(entity, "utf8") : entity.length;
|
|
759
|
+
const tag = `"${len.toString(16)}-${crypto.hash("sha1", entity, "base64").substring(0, 27)}"`;
|
|
760
|
+
return weak ? "W/" + tag : tag;
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/**
|
|
764
|
+
* The ETag of a file, which is its size and mtime rather than its contents: send computes it this
|
|
765
|
+
* way so that serving a large file does not mean reading it twice.
|
|
766
|
+
*
|
|
767
|
+
* @param {import("fs").Stats} stat
|
|
768
|
+
* @param {boolean} weak
|
|
769
|
+
* @returns {string}
|
|
770
|
+
*/
|
|
771
|
+
function statTag(stat, weak) {
|
|
772
|
+
const tag = `"${stat.size.toString(16)}-${stat.mtime.getTime().toString(16)}"`;
|
|
773
|
+
return weak ? "W/" + tag : tag;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* The function the "etag" setting installs. It takes either a body or an fs.Stats, since a file's
|
|
778
|
+
* ETag comes from its size and mtime while a body's comes from its contents.
|
|
779
|
+
*
|
|
780
|
+
* @param {{weak: boolean}} options
|
|
781
|
+
* @returns {(body: any, encoding?: BufferEncoding) => string}
|
|
782
|
+
*/
|
|
783
|
+
function createETagGenerator(options) {
|
|
784
|
+
return function generateETag(body, encoding) {
|
|
785
|
+
if (body instanceof Stats) {
|
|
786
|
+
return statTag(body, options.weak);
|
|
787
|
+
}
|
|
788
|
+
const buf = !Buffer.isBuffer(body) ? Buffer.from(body, encoding) : body;
|
|
789
|
+
return entityTag(buf, options.weak);
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
/**
|
|
794
|
+
* Whether an If-Range still holds, which decides between answering the range that was asked for
|
|
795
|
+
* and sending the whole file. It may carry either an ETag or a date, and a date only counts when
|
|
796
|
+
* it matches Last-Modified exactly.
|
|
797
|
+
*
|
|
798
|
+
* @param {any} req
|
|
799
|
+
* @param {any} res
|
|
800
|
+
* @returns {boolean}
|
|
801
|
+
*/
|
|
802
|
+
function isRangeFresh(req, res) {
|
|
803
|
+
const ifRange = req.headers["if-range"];
|
|
804
|
+
if (!ifRange) {
|
|
805
|
+
return true;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// if-range as etag
|
|
809
|
+
if (ifRange.indexOf('"') !== -1) {
|
|
810
|
+
const etag = res.get("etag");
|
|
811
|
+
return Boolean(etag && ifRange.indexOf(etag) !== -1);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// if-range as modified date
|
|
815
|
+
const lastModified = res.get("Last-Modified");
|
|
816
|
+
return parseHttpDate(lastModified) <= parseHttpDate(ifRange);
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Escapes the five characters that would otherwise be markup. Written as a scan rather than a
|
|
821
|
+
* chain of replaces because it runs on every error page and every redirect body.
|
|
822
|
+
*
|
|
823
|
+
* @param {string} str
|
|
824
|
+
* @returns {string}
|
|
825
|
+
*/
|
|
826
|
+
function escapeHtml(str) {
|
|
827
|
+
const s = String(str);
|
|
828
|
+
const len = s.length;
|
|
829
|
+
let i = 0;
|
|
830
|
+
|
|
831
|
+
// Fast scan: find first char that needs escaping
|
|
832
|
+
for (; i < len; i++) {
|
|
833
|
+
const ch = s.charCodeAt(i);
|
|
834
|
+
if (ch === 0x26 || ch === 0x3c || ch === 0x3e || ch === 0x22 || ch === 0x27) {
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// No escaping needed
|
|
840
|
+
if (i === len) return s;
|
|
841
|
+
|
|
842
|
+
// Build escaped string from the first match onward
|
|
843
|
+
let escaped = s.substring(0, i);
|
|
844
|
+
|
|
845
|
+
for (; i < len; i++) {
|
|
846
|
+
const ch = s.charCodeAt(i);
|
|
847
|
+
switch (ch) {
|
|
848
|
+
case 0x26: // &
|
|
849
|
+
escaped += "&";
|
|
850
|
+
break;
|
|
851
|
+
case 0x3c: // <
|
|
852
|
+
escaped += "<";
|
|
853
|
+
break;
|
|
854
|
+
case 0x3e: // >
|
|
855
|
+
escaped += ">";
|
|
856
|
+
break;
|
|
857
|
+
case 0x22: // "
|
|
858
|
+
escaped += """;
|
|
859
|
+
break;
|
|
860
|
+
case 0x27: // '
|
|
861
|
+
escaped += "'";
|
|
862
|
+
break;
|
|
863
|
+
default:
|
|
864
|
+
escaped += s.charAt(i);
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
return escaped;
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// a charset parameter that is already there, and the whole parameter so it can be replaced
|
|
873
|
+
const CHARSET_PRESENT = /;\s*charset\s*=/i;
|
|
874
|
+
const CHARSET_PARAM = /;\s*charset\s*=\s*[^;]*/i;
|
|
875
|
+
const UTF8_CHARSET = "; charset=utf-8";
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* The value plus the charset its media type implies: text/* gets one, and so does any type whose
|
|
879
|
+
* mime database entry names one. Memoized, since an application sends two or three content-types
|
|
880
|
+
* and working it out again costs 159ns against 7.
|
|
881
|
+
*
|
|
882
|
+
* @param {string} value
|
|
883
|
+
* @returns {string}
|
|
884
|
+
*/
|
|
885
|
+
const withDefaultCharset = memoizeByString((value) => {
|
|
886
|
+
if (CHARSET_PRESENT.test(value)) {
|
|
887
|
+
return value;
|
|
888
|
+
}
|
|
889
|
+
const charset = mime.charset(value.split(";")[0]);
|
|
890
|
+
return charset ? `${value}; charset=${charset.toLowerCase()}` : value;
|
|
891
|
+
});
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* The same content-type, saying utf-8. A string body is written as utf-8 whatever the header
|
|
895
|
+
* claimed, so a header claiming otherwise is wrong on the wire, and Express replaces it too.
|
|
896
|
+
*
|
|
897
|
+
* @param {string} value
|
|
898
|
+
* @returns {string}
|
|
899
|
+
*/
|
|
900
|
+
function withUtf8Charset(value) {
|
|
901
|
+
// almost every string body already carries the header in this exact form, and the pair of
|
|
902
|
+
// regular expressions below was 2% of the time spent serving a request
|
|
903
|
+
if (value.endsWith(UTF8_CHARSET)) {
|
|
904
|
+
return value;
|
|
905
|
+
}
|
|
906
|
+
return CHARSET_PARAM.test(value) ? value.replace(CHARSET_PARAM, UTF8_CHARSET) : `${value}${UTF8_CHARSET}`;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// The status send picks for a failed stat. Anything else is the file being there but unreadable,
|
|
910
|
+
// which is the server's problem and not the request's.
|
|
911
|
+
const STAT_ERROR_STATUS = { ENAMETOOLONG: 404, ENOTDIR: 404, ENOENT: 404 };
|
|
912
|
+
|
|
913
|
+
/**
|
|
914
|
+
* The error send and serve-static refuse with, so that the file serving here refuses the same way.
|
|
915
|
+
*
|
|
916
|
+
* They build these with http-errors, so they carry `status`, `statusCode` and `expose`, which is
|
|
917
|
+
* what `res.status(err.status || 500)` reads. Without a status a 403, a 404 and a 416 all came out
|
|
918
|
+
* of that handler as 500. The message is the status's own name, as http-errors writes it.
|
|
919
|
+
*
|
|
920
|
+
* @param {number} status
|
|
921
|
+
* @returns {any}
|
|
922
|
+
*/
|
|
923
|
+
function httpError(status) {
|
|
924
|
+
const err = /** @type {any} */ (new Error(statuses.message[status]));
|
|
925
|
+
err.expose = status < 500;
|
|
926
|
+
err.statusCode = status;
|
|
927
|
+
err.status = status;
|
|
928
|
+
return err;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Marks an fs error the way send does before it is handed on, so an error handler reading
|
|
933
|
+
* err.status or err.statusCode finds what it would find behind Express. The three properties are
|
|
934
|
+
* assigned in this order because they are serialised in insertion order, and an error handler that
|
|
935
|
+
* answers with res.send(err) sends them.
|
|
936
|
+
*
|
|
937
|
+
* The error itself is returned rather than a new one, so its errno, code, syscall and path survive.
|
|
938
|
+
*
|
|
939
|
+
* @param {any} err
|
|
940
|
+
* @returns {any} the same error
|
|
941
|
+
*/
|
|
942
|
+
function asStatError(err) {
|
|
943
|
+
err.expose = false;
|
|
944
|
+
err.statusCode = STAT_ERROR_STATUS[err.code] ?? 500;
|
|
945
|
+
err.status = err.statusCode;
|
|
946
|
+
return err;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// fast null object
|
|
950
|
+
// A constructor whose instances have no prototype, so a key from a request body or a query string
|
|
951
|
+
// cannot reach Object.prototype. Typed as returning a plain record: without that, assigning one
|
|
952
|
+
// reads as assigning `any`, which resets narrowing instead of removing undefined from it.
|
|
953
|
+
/** @type {new () => Record<string, any>} */
|
|
954
|
+
const NullObject = /** @type {any} */ (function () {});
|
|
955
|
+
NullObject.prototype = Object.create(null);
|
|
956
|
+
|
|
957
|
+
module.exports = {
|
|
958
|
+
removeDuplicateSlashes,
|
|
959
|
+
patternToRegex,
|
|
960
|
+
getPatternMeta,
|
|
961
|
+
needsConversionToRegex,
|
|
962
|
+
acceptParams,
|
|
963
|
+
normalizeType,
|
|
964
|
+
stringify,
|
|
965
|
+
defaultSettings,
|
|
966
|
+
compileTrust,
|
|
967
|
+
deprecated,
|
|
968
|
+
UP_PATH_REGEXP,
|
|
969
|
+
NullObject,
|
|
970
|
+
decode,
|
|
971
|
+
decodeParam,
|
|
972
|
+
containsDotFile,
|
|
973
|
+
parseTokenList,
|
|
974
|
+
parseHttpDate,
|
|
975
|
+
isPreconditionFailure,
|
|
976
|
+
createETagGenerator,
|
|
977
|
+
entityTag,
|
|
978
|
+
statTag,
|
|
979
|
+
contentTypeFor,
|
|
980
|
+
memoizeByString,
|
|
981
|
+
isRangeFresh,
|
|
982
|
+
findIndexStartingFrom,
|
|
983
|
+
fastQueryParse,
|
|
984
|
+
canBeOptimized,
|
|
985
|
+
canBeOptimizedWithParams,
|
|
986
|
+
pathsCanOverlap,
|
|
987
|
+
escapeHtml,
|
|
988
|
+
withDefaultCharset,
|
|
989
|
+
withUtf8Charset,
|
|
990
|
+
asStatError,
|
|
991
|
+
httpError,
|
|
992
|
+
EMPTY_REGEX
|
|
993
|
+
};
|