@orkestrel/middleware 0.0.5 → 0.0.7
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/dist/src/core/index.cjs +81 -66
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +40 -23
- package/dist/src/core/index.d.ts +40 -23
- package/dist/src/core/index.js +81 -67
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +344 -256
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +51 -0
- package/dist/src/server/index.d.ts +51 -0
- package/dist/src/server/index.js +342 -257
- package/dist/src/server/index.js.map +1 -1
- package/package.json +25 -23
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
2
|
let node_fs = require("node:fs");
|
|
3
3
|
let node_fs_promises = require("node:fs/promises");
|
|
4
|
-
let node_os = require("node:os");
|
|
5
4
|
let node_path = require("node:path");
|
|
6
|
-
let
|
|
5
|
+
let node_util = require("node:util");
|
|
6
|
+
let node_zlib = require("node:zlib");
|
|
7
7
|
let _orkestrel_contract = require("@orkestrel/contract");
|
|
8
|
+
let node_crypto = require("node:crypto");
|
|
9
|
+
let node_os = require("node:os");
|
|
8
10
|
let _orkestrel_server = require("@orkestrel/server");
|
|
9
11
|
let _src_core = require("../core/index.cjs");
|
|
10
|
-
let node_zlib = require("node:zlib");
|
|
11
|
-
let node_util = require("node:util");
|
|
12
12
|
//#region src/server/constants.ts
|
|
13
13
|
/** The HTTP status `createMultipart` renders for each {@link MultipartReason}. */
|
|
14
14
|
var MULTIPART_REASON_STATUS = Object.freeze({
|
|
@@ -115,7 +115,7 @@ var MultipartError = class extends Error {
|
|
|
115
115
|
super(message);
|
|
116
116
|
this.status = MULTIPART_REASON_STATUS[reason];
|
|
117
117
|
this.reason = reason;
|
|
118
|
-
this.context = context;
|
|
118
|
+
if (context !== void 0) this.context = context;
|
|
119
119
|
Object.defineProperty(this, Symbol.for("@orkestrel/middleware.MultipartError"), { value: true });
|
|
120
120
|
}
|
|
121
121
|
};
|
|
@@ -151,6 +151,210 @@ function isMultipartError(value) {
|
|
|
151
151
|
return true;
|
|
152
152
|
}
|
|
153
153
|
//#endregion
|
|
154
|
+
//#region src/server/MultipartParser.ts
|
|
155
|
+
var MultipartParser = class MultipartParser {
|
|
156
|
+
static #defaultDirectory;
|
|
157
|
+
#reader;
|
|
158
|
+
#signal;
|
|
159
|
+
#abort;
|
|
160
|
+
#boundary;
|
|
161
|
+
#limits;
|
|
162
|
+
#allowed;
|
|
163
|
+
#directory;
|
|
164
|
+
#staged = [];
|
|
165
|
+
#files = Object.create(null);
|
|
166
|
+
#fields = Object.create(null);
|
|
167
|
+
#buffer = Buffer.alloc(0);
|
|
168
|
+
#ended = false;
|
|
169
|
+
#fileCount = 0;
|
|
170
|
+
#fieldCount = 0;
|
|
171
|
+
#totalBytes = 0;
|
|
172
|
+
constructor(stream, signal, boundary, limits, allowed, directory) {
|
|
173
|
+
this.#reader = stream.getReader();
|
|
174
|
+
this.#signal = signal;
|
|
175
|
+
this.#abort = this.#wakeReader.bind(this);
|
|
176
|
+
this.#boundary = boundary;
|
|
177
|
+
this.#limits = limits;
|
|
178
|
+
this.#allowed = allowed;
|
|
179
|
+
this.#directory = directory;
|
|
180
|
+
}
|
|
181
|
+
static directory() {
|
|
182
|
+
if (MultipartParser.#defaultDirectory === void 0) MultipartParser.#defaultDirectory = MultipartParser.#createDirectory();
|
|
183
|
+
return MultipartParser.#defaultDirectory;
|
|
184
|
+
}
|
|
185
|
+
async parse() {
|
|
186
|
+
this.#signal.addEventListener("abort", this.#abort, { once: true });
|
|
187
|
+
try {
|
|
188
|
+
const openMarker = Buffer.from(`--${this.#boundary}`);
|
|
189
|
+
let preambleScanned = 0;
|
|
190
|
+
let index = this.#buffer.indexOf(openMarker);
|
|
191
|
+
while (index === -1) {
|
|
192
|
+
const carry = openMarker.length - 1;
|
|
193
|
+
if (this.#buffer.length > carry) {
|
|
194
|
+
const drop = this.#buffer.length - carry;
|
|
195
|
+
preambleScanned += drop;
|
|
196
|
+
if (preambleScanned > 65536) throw new MultipartError("malformed", "multipart preamble too large");
|
|
197
|
+
this.#buffer = this.#buffer.subarray(drop);
|
|
198
|
+
}
|
|
199
|
+
if (!await this.#pull()) throw new MultipartError("malformed", "missing multipart boundary");
|
|
200
|
+
index = this.#buffer.indexOf(openMarker);
|
|
201
|
+
}
|
|
202
|
+
if (preambleScanned + index > 65536) throw new MultipartError("malformed", "multipart preamble too large");
|
|
203
|
+
this.#buffer = this.#buffer.subarray(index + openMarker.length);
|
|
204
|
+
for (;;) {
|
|
205
|
+
while (this.#buffer.length < 2) if (!await this.#pull()) throw new MultipartError("malformed", "unterminated multipart boundary");
|
|
206
|
+
if (this.#buffer[0] === 45 && this.#buffer[1] === 45) break;
|
|
207
|
+
if (this.#buffer[0] !== 13 || this.#buffer[1] !== 10) throw new MultipartError("malformed", "malformed multipart boundary");
|
|
208
|
+
this.#buffer = this.#buffer.subarray(2);
|
|
209
|
+
let headerEnd = this.#buffer.indexOf("\r\n\r\n");
|
|
210
|
+
while (headerEnd === -1) {
|
|
211
|
+
if (this.#buffer.length > 16384) throw new MultipartError("malformed", "multipart header block too large");
|
|
212
|
+
if (!await this.#pull()) throw new MultipartError("malformed", "unterminated multipart part headers");
|
|
213
|
+
headerEnd = this.#buffer.indexOf("\r\n\r\n");
|
|
214
|
+
}
|
|
215
|
+
if (headerEnd > 16384) throw new MultipartError("malformed", "multipart header block too large");
|
|
216
|
+
const headerBlock = this.#buffer.subarray(0, headerEnd).toString("utf8");
|
|
217
|
+
this.#buffer = this.#buffer.subarray(headerEnd + 4);
|
|
218
|
+
const { name, filename, contentType } = parsePartHeaders(headerBlock);
|
|
219
|
+
if (name === void 0) throw new MultipartError("malformed", "multipart part missing name");
|
|
220
|
+
const partDelimiter = Buffer.from(`\r\n--${this.#boundary}`);
|
|
221
|
+
if (filename !== void 0) {
|
|
222
|
+
if (filename !== "") {
|
|
223
|
+
this.#fileCount += 1;
|
|
224
|
+
if (this.#fileCount > this.#limits.files) throw new MultipartError("limit", "too many multipart files");
|
|
225
|
+
}
|
|
226
|
+
const path = (0, node_path.join)(this.#directory, (0, node_crypto.randomUUID)());
|
|
227
|
+
this.#staged.push(path);
|
|
228
|
+
const handle = await (0, node_fs_promises.open)(path, "w", 384);
|
|
229
|
+
let size = 0;
|
|
230
|
+
let head = Buffer.alloc(0);
|
|
231
|
+
try {
|
|
232
|
+
for (;;) {
|
|
233
|
+
const boundaryIndex = this.#buffer.indexOf(partDelimiter);
|
|
234
|
+
if (boundaryIndex === -1) {
|
|
235
|
+
const safeLength = Math.max(0, this.#buffer.length - (partDelimiter.length - 1));
|
|
236
|
+
if (safeLength > 0) {
|
|
237
|
+
const chunk = this.#buffer.subarray(0, safeLength);
|
|
238
|
+
size += chunk.length;
|
|
239
|
+
if (size > this.#limits.file) throw new MultipartError("limit", "multipart file exceeds size limit");
|
|
240
|
+
if (head.length < 16) head = Buffer.concat([head, chunk.subarray(0, 16 - head.length)]);
|
|
241
|
+
await handle.write(chunk);
|
|
242
|
+
this.#buffer = this.#buffer.subarray(safeLength);
|
|
243
|
+
}
|
|
244
|
+
if (!await this.#pull()) throw new MultipartError("malformed", "unterminated multipart file part");
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
const chunk = this.#buffer.subarray(0, boundaryIndex);
|
|
248
|
+
size += chunk.length;
|
|
249
|
+
if (size > this.#limits.file) throw new MultipartError("limit", "multipart file exceeds size limit");
|
|
250
|
+
if (head.length < 16) head = Buffer.concat([head, chunk.subarray(0, 16 - head.length)]);
|
|
251
|
+
await handle.write(chunk);
|
|
252
|
+
this.#buffer = this.#buffer.subarray(boundaryIndex + 2);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
} finally {
|
|
256
|
+
await handle.close();
|
|
257
|
+
}
|
|
258
|
+
if (filename === "" && size === 0) {
|
|
259
|
+
await (0, node_fs_promises.unlink)(path);
|
|
260
|
+
this.#staged.splice(this.#staged.indexOf(path), 1);
|
|
261
|
+
} else {
|
|
262
|
+
if (filename === "") {
|
|
263
|
+
this.#fileCount += 1;
|
|
264
|
+
if (this.#fileCount > this.#limits.files) throw new MultipartError("limit", "too many multipart files");
|
|
265
|
+
}
|
|
266
|
+
const detected = detectMIME(head);
|
|
267
|
+
const declared = contentType ?? "application/octet-stream";
|
|
268
|
+
const validated = detected !== void 0 && detected === declared;
|
|
269
|
+
if (this.#allowed !== void 0) {
|
|
270
|
+
if (!(detected !== void 0 && this.#allowed.includes(detected))) throw new MultipartError("rejected", "multipart file failed type validation");
|
|
271
|
+
}
|
|
272
|
+
if ((0, _orkestrel_server.isDangerousKey)(name)) {
|
|
273
|
+
await (0, node_fs_promises.unlink)(path);
|
|
274
|
+
this.#staged.splice(this.#staged.indexOf(path), 1);
|
|
275
|
+
} else {
|
|
276
|
+
const record = createUploadedFile({
|
|
277
|
+
field: name,
|
|
278
|
+
name: filename,
|
|
279
|
+
size,
|
|
280
|
+
mime: detected ?? declared,
|
|
281
|
+
validated,
|
|
282
|
+
status: "staged",
|
|
283
|
+
path
|
|
284
|
+
});
|
|
285
|
+
const existing = this.#files[name];
|
|
286
|
+
if (existing === void 0) this.#files[name] = [record];
|
|
287
|
+
else existing.push(record);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} else {
|
|
291
|
+
this.#fieldCount += 1;
|
|
292
|
+
if (this.#fieldCount > this.#limits.fields) throw new MultipartError("limit", "too many multipart fields");
|
|
293
|
+
let value = Buffer.alloc(0);
|
|
294
|
+
for (;;) {
|
|
295
|
+
const boundaryIndex = this.#buffer.indexOf(partDelimiter);
|
|
296
|
+
if (boundaryIndex === -1) {
|
|
297
|
+
const safeLength = Math.max(0, this.#buffer.length - (partDelimiter.length - 1));
|
|
298
|
+
if (safeLength > 0) {
|
|
299
|
+
value = Buffer.concat([value, this.#buffer.subarray(0, safeLength)]);
|
|
300
|
+
if (value.length > this.#limits.field) throw new MultipartError("limit", "multipart field exceeds size limit");
|
|
301
|
+
this.#buffer = this.#buffer.subarray(safeLength);
|
|
302
|
+
}
|
|
303
|
+
if (!await this.#pull()) throw new MultipartError("malformed", "unterminated multipart field part");
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
value = Buffer.concat([value, this.#buffer.subarray(0, boundaryIndex)]);
|
|
307
|
+
if (value.length > this.#limits.field) throw new MultipartError("limit", "multipart field exceeds size limit");
|
|
308
|
+
this.#buffer = this.#buffer.subarray(boundaryIndex + 2);
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
if (!(0, _orkestrel_server.isDangerousKey)(name)) this.#fields[name] = value.toString("utf8");
|
|
312
|
+
}
|
|
313
|
+
while (this.#buffer.length < openMarker.length) if (!await this.#pull()) throw new MultipartError("malformed", "unterminated multipart boundary");
|
|
314
|
+
this.#buffer = this.#buffer.subarray(openMarker.length);
|
|
315
|
+
}
|
|
316
|
+
} catch (error) {
|
|
317
|
+
await this.#cleanup();
|
|
318
|
+
await this.#reader.cancel().catch(() => {});
|
|
319
|
+
throw error;
|
|
320
|
+
} finally {
|
|
321
|
+
this.#signal.removeEventListener("abort", this.#abort);
|
|
322
|
+
if (!this.#ended) await this.#reader.cancel().catch(() => {});
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
files: Object.freeze(this.#files),
|
|
326
|
+
fields: Object.freeze(this.#fields)
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
static async #createDirectory() {
|
|
330
|
+
const path = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "orkestrel-multipart-"));
|
|
331
|
+
await (0, node_fs_promises.chmod)(path, 448);
|
|
332
|
+
return path;
|
|
333
|
+
}
|
|
334
|
+
async #cleanup() {
|
|
335
|
+
for (const path of this.#staged) try {
|
|
336
|
+
await (0, node_fs_promises.unlink)(path);
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
339
|
+
async #pull() {
|
|
340
|
+
if (this.#signal.aborted) throw new MultipartError("malformed", "request aborted mid-upload");
|
|
341
|
+
if (this.#ended) return false;
|
|
342
|
+
const { done, value } = await this.#reader.read();
|
|
343
|
+
if (this.#signal.aborted) throw new MultipartError("malformed", "request aborted mid-upload");
|
|
344
|
+
if (done) {
|
|
345
|
+
this.#ended = true;
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
this.#totalBytes += value.byteLength;
|
|
349
|
+
if (this.#totalBytes > this.#limits.total) throw new MultipartError("limit", "multipart body exceeds total limit");
|
|
350
|
+
this.#buffer = Buffer.concat([this.#buffer, Buffer.from(value.buffer, value.byteOffset, value.byteLength)]);
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
#wakeReader() {
|
|
354
|
+
this.#reader.cancel().catch(() => {});
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
//#endregion
|
|
154
358
|
//#region src/server/helpers.ts
|
|
155
359
|
/**
|
|
156
360
|
* Whether `pathname` is `prefix` itself or lies under it on a SEGMENT
|
|
@@ -174,6 +378,31 @@ function isUnderPath(pathname, prefix) {
|
|
|
174
378
|
return pathname.startsWith(boundary);
|
|
175
379
|
}
|
|
176
380
|
/**
|
|
381
|
+
* Resolve the fixed SPA shell path when a static-file miss is eligible for
|
|
382
|
+
* fallback.
|
|
383
|
+
*
|
|
384
|
+
* @param root - The configured static root
|
|
385
|
+
* @param index - The configured shell filename
|
|
386
|
+
* @param exclude - The URL prefix excluded from fallback
|
|
387
|
+
* @param method - The request method
|
|
388
|
+
* @param pathname - The request pathname
|
|
389
|
+
* @param accept - The request's `Accept` header value
|
|
390
|
+
* @returns The fixed shell path, or `undefined` when fallback is ineligible
|
|
391
|
+
*
|
|
392
|
+
* @example
|
|
393
|
+
* ```ts
|
|
394
|
+
* resolveStaticFallbackPath('/srv/public', 'index.html', '/api', 'GET', '/dashboard', 'text/html')
|
|
395
|
+
* // '/srv/public/index.html'
|
|
396
|
+
* ```
|
|
397
|
+
*/
|
|
398
|
+
function resolveStaticFallbackPath(root, index, exclude, method, pathname, accept) {
|
|
399
|
+
if (method !== "GET") return void 0;
|
|
400
|
+
if ((0, node_path.extname)(pathname) !== "") return void 0;
|
|
401
|
+
if (!accept.includes("text/html") && !accept.includes("*/*")) return void 0;
|
|
402
|
+
if (isUnderPath(pathname, exclude)) return void 0;
|
|
403
|
+
return (0, node_path.join)(root, index);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
177
406
|
* Whether `child` is `parent` itself or lies inside it on-disk — the
|
|
178
407
|
* FILESYSTEM containment predicate `createStatic` applies to `fs.realpath`
|
|
179
408
|
* output (never to a URL pathname — that is {@link isUnderPath}'s job).
|
|
@@ -236,7 +465,8 @@ function resolveStaticPath(root, prefix, pathname) {
|
|
|
236
465
|
return;
|
|
237
466
|
}
|
|
238
467
|
if (decoded.includes("\0")) return void 0;
|
|
239
|
-
const
|
|
468
|
+
const stripped = decoded.replace(/^[/\\]+/, "");
|
|
469
|
+
const normalized = (0, node_path.normalize)(stripped);
|
|
240
470
|
const segments = normalized.split(/[/\\]+/).filter((segment) => segment.length > 0);
|
|
241
471
|
for (const segment of segments) if (isReservedDeviceName(segment)) return void 0;
|
|
242
472
|
const resolved = (0, node_path.resolve)(root, normalized);
|
|
@@ -313,6 +543,23 @@ function computeFileETag(size, mtimeMs) {
|
|
|
313
543
|
return `W/"${size}-${Math.floor(mtimeMs)}"`;
|
|
314
544
|
}
|
|
315
545
|
/**
|
|
546
|
+
* Compress response bytes with Node's guaranteed zlib gzip/deflate codecs.
|
|
547
|
+
*
|
|
548
|
+
* @param bytes - The uncompressed response bytes
|
|
549
|
+
* @param encoding - The negotiated actionable coding
|
|
550
|
+
* @returns The compressed bytes
|
|
551
|
+
*
|
|
552
|
+
* @example
|
|
553
|
+
* ```ts
|
|
554
|
+
* const bytes = new TextEncoder().encode('compress me')
|
|
555
|
+
* const compressed = await compressNodeBytes(bytes, 'deflate')
|
|
556
|
+
* ```
|
|
557
|
+
*/
|
|
558
|
+
async function compressNodeBytes(bytes, encoding) {
|
|
559
|
+
const compressed = encoding === "gzip" ? await (0, node_util.promisify)(node_zlib.gzip)(bytes) : await (0, node_util.promisify)(node_zlib.deflate)(bytes);
|
|
560
|
+
return Uint8Array.from(compressed);
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
316
563
|
* Sniff a MIME type from a file's leading bytes against a small magic-byte
|
|
317
564
|
* table (jpeg, png, gif87a/89a, webp, pdf, zip) — the SNIFF-AUTHORITATIVE
|
|
318
565
|
* signal `createMultipart`'s type validation rests on, never the declared
|
|
@@ -327,17 +574,12 @@ function computeFileETag(size, mtimeMs) {
|
|
|
327
574
|
* ```
|
|
328
575
|
*/
|
|
329
576
|
function detectMIME(head) {
|
|
330
|
-
|
|
331
|
-
if (head.length < offset + signature.length) return false;
|
|
332
|
-
for (let index = 0; index < signature.length; index += 1) if (head[offset + index] !== signature[index]) return false;
|
|
333
|
-
return true;
|
|
334
|
-
}
|
|
335
|
-
if (matches([
|
|
577
|
+
if (matchesBytes(head, [
|
|
336
578
|
255,
|
|
337
579
|
216,
|
|
338
580
|
255
|
|
339
581
|
])) return "image/jpeg";
|
|
340
|
-
if (
|
|
582
|
+
if (matchesBytes(head, [
|
|
341
583
|
137,
|
|
342
584
|
80,
|
|
343
585
|
78,
|
|
@@ -347,7 +589,7 @@ function detectMIME(head) {
|
|
|
347
589
|
26,
|
|
348
590
|
10
|
|
349
591
|
])) return "image/png";
|
|
350
|
-
if (
|
|
592
|
+
if (matchesBytes(head, [
|
|
351
593
|
71,
|
|
352
594
|
73,
|
|
353
595
|
70,
|
|
@@ -355,7 +597,7 @@ function detectMIME(head) {
|
|
|
355
597
|
55,
|
|
356
598
|
97
|
|
357
599
|
])) return "image/gif";
|
|
358
|
-
if (
|
|
600
|
+
if (matchesBytes(head, [
|
|
359
601
|
71,
|
|
360
602
|
73,
|
|
361
603
|
70,
|
|
@@ -363,35 +605,35 @@ function detectMIME(head) {
|
|
|
363
605
|
57,
|
|
364
606
|
97
|
|
365
607
|
])) return "image/gif";
|
|
366
|
-
if (
|
|
608
|
+
if (matchesBytes(head, [
|
|
367
609
|
82,
|
|
368
610
|
73,
|
|
369
611
|
70,
|
|
370
612
|
70
|
|
371
|
-
]) &&
|
|
613
|
+
]) && matchesBytes(head, [
|
|
372
614
|
87,
|
|
373
615
|
69,
|
|
374
616
|
66,
|
|
375
617
|
80
|
|
376
618
|
], 8)) return "image/webp";
|
|
377
|
-
if (
|
|
619
|
+
if (matchesBytes(head, [
|
|
378
620
|
37,
|
|
379
621
|
80,
|
|
380
622
|
68,
|
|
381
623
|
70,
|
|
382
624
|
45
|
|
383
625
|
])) return "application/pdf";
|
|
384
|
-
if (
|
|
626
|
+
if (matchesBytes(head, [
|
|
385
627
|
80,
|
|
386
628
|
75,
|
|
387
629
|
3,
|
|
388
630
|
4
|
|
389
|
-
]) ||
|
|
631
|
+
]) || matchesBytes(head, [
|
|
390
632
|
80,
|
|
391
633
|
75,
|
|
392
634
|
5,
|
|
393
635
|
6
|
|
394
|
-
]) ||
|
|
636
|
+
]) || matchesBytes(head, [
|
|
395
637
|
80,
|
|
396
638
|
75,
|
|
397
639
|
7,
|
|
@@ -399,6 +641,24 @@ function detectMIME(head) {
|
|
|
399
641
|
])) return "application/zip";
|
|
400
642
|
}
|
|
401
643
|
/**
|
|
644
|
+
* Whether `bytes` contains `signature` at the requested offset.
|
|
645
|
+
*
|
|
646
|
+
* @param bytes - The bytes to inspect
|
|
647
|
+
* @param signature - The exact byte sequence to match
|
|
648
|
+
* @param offset - The starting byte offset, defaulting to zero
|
|
649
|
+
* @returns `true` when the complete signature matches
|
|
650
|
+
*
|
|
651
|
+
* @example
|
|
652
|
+
* ```ts
|
|
653
|
+
* matchesBytes(Uint8Array.from([0, 0x50, 0x4b]), [0x50, 0x4b], 1) // true
|
|
654
|
+
* ```
|
|
655
|
+
*/
|
|
656
|
+
function matchesBytes(bytes, signature, offset = 0) {
|
|
657
|
+
if (bytes.length < offset + signature.length) return false;
|
|
658
|
+
for (let index = 0; index < signature.length; index += 1) if (bytes[offset + index] !== signature[index]) return false;
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
402
662
|
* Extract the `boundary` parameter from a `Content-Type` header, or
|
|
403
663
|
* `undefined` when the request is not `multipart/form-data`.
|
|
404
664
|
*
|
|
@@ -442,11 +702,6 @@ function resolveMultipartLimits(limits) {
|
|
|
442
702
|
};
|
|
443
703
|
}
|
|
444
704
|
/**
|
|
445
|
-
* Memoized `Promise` for `parseMultipartRequest`'s lazily-created default
|
|
446
|
-
* staging directory — created once per process via {@link resolveDefaultDirectory}.
|
|
447
|
-
*/
|
|
448
|
-
var defaultDirectory;
|
|
449
|
-
/**
|
|
450
705
|
* Resolve `parseMultipartRequest`'s default staging directory when the
|
|
451
706
|
* caller did not configure one — a process-owned directory created ONCE
|
|
452
707
|
* (lazily, memoized across calls) via `mkdtemp` under `os.tmpdir()` and
|
|
@@ -460,12 +715,7 @@ var defaultDirectory;
|
|
|
460
715
|
* ```
|
|
461
716
|
*/
|
|
462
717
|
function resolveDefaultDirectory() {
|
|
463
|
-
|
|
464
|
-
const path = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "orkestrel-multipart-"));
|
|
465
|
-
await (0, node_fs_promises.chmod)(path, 448);
|
|
466
|
-
return path;
|
|
467
|
-
})();
|
|
468
|
-
return defaultDirectory;
|
|
718
|
+
return MultipartParser.directory();
|
|
469
719
|
}
|
|
470
720
|
/**
|
|
471
721
|
* Parse one multipart part's raw header block into its `name` (from
|
|
@@ -552,177 +802,7 @@ async function parseMultipartRequest(request, options = {}) {
|
|
|
552
802
|
const limits = resolveMultipartLimits(options.limits);
|
|
553
803
|
const allowed = options.allowed;
|
|
554
804
|
const directory = options.directory ?? await resolveDefaultDirectory();
|
|
555
|
-
|
|
556
|
-
const files = Object.create(null);
|
|
557
|
-
const fields = Object.create(null);
|
|
558
|
-
let fileCount = 0;
|
|
559
|
-
let fieldCount = 0;
|
|
560
|
-
let totalBytes = 0;
|
|
561
|
-
let aborted = false;
|
|
562
|
-
async function cleanup() {
|
|
563
|
-
for (const path of staged) try {
|
|
564
|
-
await (0, node_fs_promises.unlink)(path);
|
|
565
|
-
} catch {}
|
|
566
|
-
}
|
|
567
|
-
const reader = request.body.getReader();
|
|
568
|
-
let buffer = Buffer.alloc(0);
|
|
569
|
-
let ended = false;
|
|
570
|
-
const onAbort = () => {
|
|
571
|
-
aborted = true;
|
|
572
|
-
};
|
|
573
|
-
request.signal.addEventListener("abort", onAbort);
|
|
574
|
-
async function pull() {
|
|
575
|
-
if (aborted || request.signal.aborted) throw new MultipartError("malformed", "request aborted mid-upload");
|
|
576
|
-
if (ended) return false;
|
|
577
|
-
const { done, value } = await reader.read();
|
|
578
|
-
if (done) {
|
|
579
|
-
ended = true;
|
|
580
|
-
return false;
|
|
581
|
-
}
|
|
582
|
-
totalBytes += value.byteLength;
|
|
583
|
-
if (totalBytes > limits.total) throw new MultipartError("limit", "multipart body exceeds total limit");
|
|
584
|
-
buffer = Buffer.concat([buffer, Buffer.from(value.buffer, value.byteOffset, value.byteLength)]);
|
|
585
|
-
return true;
|
|
586
|
-
}
|
|
587
|
-
try {
|
|
588
|
-
const openMarker = Buffer.from(`--${boundary}`);
|
|
589
|
-
let preambleScanned = 0;
|
|
590
|
-
let index = buffer.indexOf(openMarker);
|
|
591
|
-
while (index === -1) {
|
|
592
|
-
const carry = openMarker.length - 1;
|
|
593
|
-
if (buffer.length > carry) {
|
|
594
|
-
const drop = buffer.length - carry;
|
|
595
|
-
preambleScanned += drop;
|
|
596
|
-
if (preambleScanned > 65536) throw new MultipartError("malformed", "multipart preamble too large");
|
|
597
|
-
buffer = buffer.subarray(drop);
|
|
598
|
-
}
|
|
599
|
-
if (!await pull()) throw new MultipartError("malformed", "missing multipart boundary");
|
|
600
|
-
index = buffer.indexOf(openMarker);
|
|
601
|
-
}
|
|
602
|
-
buffer = buffer.subarray(index + openMarker.length);
|
|
603
|
-
for (;;) {
|
|
604
|
-
while (buffer.length < 2) if (!await pull()) throw new MultipartError("malformed", "unterminated multipart boundary");
|
|
605
|
-
if (buffer[0] === 45 && buffer[1] === 45) break;
|
|
606
|
-
if (buffer[0] !== 13 || buffer[1] !== 10) throw new MultipartError("malformed", "malformed multipart boundary");
|
|
607
|
-
buffer = buffer.subarray(2);
|
|
608
|
-
let headerEnd = buffer.indexOf("\r\n\r\n");
|
|
609
|
-
while (headerEnd === -1) {
|
|
610
|
-
if (buffer.length > 16384) throw new MultipartError("malformed", "multipart header block too large");
|
|
611
|
-
if (!await pull()) throw new MultipartError("malformed", "unterminated multipart part headers");
|
|
612
|
-
headerEnd = buffer.indexOf("\r\n\r\n");
|
|
613
|
-
}
|
|
614
|
-
const headerBlock = buffer.subarray(0, headerEnd).toString("utf8");
|
|
615
|
-
buffer = buffer.subarray(headerEnd + 4);
|
|
616
|
-
const { name, filename, contentType } = parsePartHeaders(headerBlock);
|
|
617
|
-
if (name === void 0) throw new MultipartError("malformed", "multipart part missing name");
|
|
618
|
-
const partDelimiter = Buffer.from(`\r\n--${boundary}`);
|
|
619
|
-
if (filename !== void 0) {
|
|
620
|
-
if (filename !== "") {
|
|
621
|
-
fileCount += 1;
|
|
622
|
-
if (fileCount > limits.files) throw new MultipartError("limit", "too many multipart files");
|
|
623
|
-
}
|
|
624
|
-
const path = (0, node_path.join)(directory, (0, node_crypto.randomUUID)());
|
|
625
|
-
staged.push(path);
|
|
626
|
-
const handle = await (0, node_fs_promises.open)(path, "w", 384);
|
|
627
|
-
let size = 0;
|
|
628
|
-
let head = Buffer.alloc(0);
|
|
629
|
-
try {
|
|
630
|
-
for (;;) {
|
|
631
|
-
const boundaryIndex = buffer.indexOf(partDelimiter);
|
|
632
|
-
if (boundaryIndex === -1) {
|
|
633
|
-
const safeLength = Math.max(0, buffer.length - (partDelimiter.length - 1));
|
|
634
|
-
if (safeLength > 0) {
|
|
635
|
-
const chunk = buffer.subarray(0, safeLength);
|
|
636
|
-
size += chunk.length;
|
|
637
|
-
if (size > limits.file) throw new MultipartError("limit", "multipart file exceeds size limit");
|
|
638
|
-
if (head.length < 16) head = Buffer.concat([head, chunk.subarray(0, 16 - head.length)]);
|
|
639
|
-
await handle.write(chunk);
|
|
640
|
-
buffer = buffer.subarray(safeLength);
|
|
641
|
-
}
|
|
642
|
-
if (!await pull()) throw new MultipartError("malformed", "unterminated multipart file part");
|
|
643
|
-
continue;
|
|
644
|
-
}
|
|
645
|
-
const chunk = buffer.subarray(0, boundaryIndex);
|
|
646
|
-
size += chunk.length;
|
|
647
|
-
if (size > limits.file) throw new MultipartError("limit", "multipart file exceeds size limit");
|
|
648
|
-
if (head.length < 16) head = Buffer.concat([head, chunk.subarray(0, 16 - head.length)]);
|
|
649
|
-
await handle.write(chunk);
|
|
650
|
-
buffer = buffer.subarray(boundaryIndex + 2);
|
|
651
|
-
break;
|
|
652
|
-
}
|
|
653
|
-
} finally {
|
|
654
|
-
await handle.close();
|
|
655
|
-
}
|
|
656
|
-
if (filename === "" && size === 0) {
|
|
657
|
-
await (0, node_fs_promises.unlink)(path);
|
|
658
|
-
staged.splice(staged.indexOf(path), 1);
|
|
659
|
-
} else {
|
|
660
|
-
if (filename === "") {
|
|
661
|
-
fileCount += 1;
|
|
662
|
-
if (fileCount > limits.files) throw new MultipartError("limit", "too many multipart files");
|
|
663
|
-
}
|
|
664
|
-
const detected = detectMIME(head);
|
|
665
|
-
const declared = contentType ?? "application/octet-stream";
|
|
666
|
-
const validated = detected !== void 0 && detected === declared;
|
|
667
|
-
if (allowed !== void 0) {
|
|
668
|
-
if (!(detected !== void 0 && allowed.includes(detected))) throw new MultipartError("rejected", "multipart file failed type validation");
|
|
669
|
-
}
|
|
670
|
-
if ((0, _orkestrel_server.isDangerousKey)(name)) {
|
|
671
|
-
await (0, node_fs_promises.unlink)(path);
|
|
672
|
-
staged.splice(staged.indexOf(path), 1);
|
|
673
|
-
} else {
|
|
674
|
-
const record = createUploadedFile({
|
|
675
|
-
field: name,
|
|
676
|
-
name: filename,
|
|
677
|
-
size,
|
|
678
|
-
mime: detected ?? declared,
|
|
679
|
-
validated,
|
|
680
|
-
status: "staged",
|
|
681
|
-
path
|
|
682
|
-
});
|
|
683
|
-
const existing = files[name];
|
|
684
|
-
if (existing === void 0) files[name] = [record];
|
|
685
|
-
else existing.push(record);
|
|
686
|
-
}
|
|
687
|
-
}
|
|
688
|
-
} else {
|
|
689
|
-
fieldCount += 1;
|
|
690
|
-
if (fieldCount > limits.fields) throw new MultipartError("limit", "too many multipart fields");
|
|
691
|
-
let value = Buffer.alloc(0);
|
|
692
|
-
for (;;) {
|
|
693
|
-
const boundaryIndex = buffer.indexOf(partDelimiter);
|
|
694
|
-
if (boundaryIndex === -1) {
|
|
695
|
-
const safeLength = Math.max(0, buffer.length - (partDelimiter.length - 1));
|
|
696
|
-
if (safeLength > 0) {
|
|
697
|
-
value = Buffer.concat([value, buffer.subarray(0, safeLength)]);
|
|
698
|
-
if (value.length > limits.field) throw new MultipartError("limit", "multipart field exceeds size limit");
|
|
699
|
-
buffer = buffer.subarray(safeLength);
|
|
700
|
-
}
|
|
701
|
-
if (!await pull()) throw new MultipartError("malformed", "unterminated multipart field part");
|
|
702
|
-
continue;
|
|
703
|
-
}
|
|
704
|
-
value = Buffer.concat([value, buffer.subarray(0, boundaryIndex)]);
|
|
705
|
-
if (value.length > limits.field) throw new MultipartError("limit", "multipart field exceeds size limit");
|
|
706
|
-
buffer = buffer.subarray(boundaryIndex + 2);
|
|
707
|
-
break;
|
|
708
|
-
}
|
|
709
|
-
if (!(0, _orkestrel_server.isDangerousKey)(name)) fields[name] = value.toString("utf8");
|
|
710
|
-
}
|
|
711
|
-
while (buffer.length < openMarker.length) if (!await pull()) throw new MultipartError("malformed", "unterminated multipart boundary");
|
|
712
|
-
buffer = buffer.subarray(openMarker.length);
|
|
713
|
-
}
|
|
714
|
-
} catch (error) {
|
|
715
|
-
await cleanup();
|
|
716
|
-
await reader.cancel().catch(() => {});
|
|
717
|
-
throw error;
|
|
718
|
-
} finally {
|
|
719
|
-
request.signal.removeEventListener("abort", onAbort);
|
|
720
|
-
if (!ended) await reader.cancel().catch(() => {});
|
|
721
|
-
}
|
|
722
|
-
return {
|
|
723
|
-
files: Object.freeze(files),
|
|
724
|
-
fields: Object.freeze(fields)
|
|
725
|
-
};
|
|
805
|
+
return new MultipartParser(request.body, request.signal, boundary, limits, allowed, directory).parse();
|
|
726
806
|
}
|
|
727
807
|
/**
|
|
728
808
|
* Build a frozen {@link UploadedFileInterface} record.
|
|
@@ -935,10 +1015,6 @@ function createStatic(options) {
|
|
|
935
1015
|
const useETag = options.etag ?? true;
|
|
936
1016
|
const fallback = options.fallback === true ? { exclude: DEFAULT_STATIC_FALLBACK_EXCLUDE } : options.fallback === false || options.fallback === void 0 ? void 0 : { exclude: options.fallback.exclude ?? "/api" };
|
|
937
1017
|
let canonicalRootPromise;
|
|
938
|
-
function canonicalRoot() {
|
|
939
|
-
if (canonicalRootPromise === void 0) canonicalRootPromise = (0, node_fs_promises.realpath)(root);
|
|
940
|
-
return canonicalRootPromise;
|
|
941
|
-
}
|
|
942
1018
|
return async (request, context, next) => {
|
|
943
1019
|
if (context.method !== "GET" && context.method !== "HEAD") return next();
|
|
944
1020
|
const target = resolveStaticPath(root, options.prefix, context.url.pathname);
|
|
@@ -948,47 +1024,83 @@ function createStatic(options) {
|
|
|
948
1024
|
if (dotfiles === "deny") throw new _orkestrel_server.HTTPError(403, "forbidden");
|
|
949
1025
|
if (dotfiles === "ignore") return next();
|
|
950
1026
|
}
|
|
951
|
-
let resolvedPath;
|
|
1027
|
+
let resolvedPath = target;
|
|
1028
|
+
let fallbackNeeded = false;
|
|
952
1029
|
try {
|
|
953
|
-
|
|
1030
|
+
if (canonicalRootPromise === void 0) canonicalRootPromise = (0, node_fs_promises.realpath)(root);
|
|
1031
|
+
const [rootReal, targetReal] = await Promise.all([canonicalRootPromise, (0, node_fs_promises.realpath)(target)]);
|
|
954
1032
|
if (!isContainedPath(targetReal, rootReal)) return next();
|
|
955
1033
|
resolvedPath = targetReal;
|
|
956
1034
|
} catch {
|
|
957
|
-
|
|
1035
|
+
fallbackNeeded = true;
|
|
958
1036
|
}
|
|
959
1037
|
let directoryInfo;
|
|
960
|
-
try {
|
|
1038
|
+
if (!fallbackNeeded) try {
|
|
961
1039
|
directoryInfo = await (0, node_fs_promises.stat)(resolvedPath);
|
|
962
1040
|
} catch {
|
|
963
|
-
|
|
1041
|
+
fallbackNeeded = true;
|
|
964
1042
|
}
|
|
965
|
-
if (directoryInfo
|
|
1043
|
+
if (directoryInfo?.isDirectory()) {
|
|
966
1044
|
resolvedPath = (0, node_path.join)(resolvedPath, index);
|
|
967
1045
|
try {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
resolvedPath = indexReal;
|
|
1046
|
+
if (canonicalRootPromise === void 0) canonicalRootPromise = (0, node_fs_promises.realpath)(root);
|
|
1047
|
+
const [rootReal, indexReal] = await Promise.all([canonicalRootPromise, (0, node_fs_promises.realpath)(resolvedPath)]);
|
|
1048
|
+
if (isContainedPath(indexReal, rootReal)) resolvedPath = indexReal;
|
|
1049
|
+
else fallbackNeeded = true;
|
|
971
1050
|
} catch {
|
|
972
|
-
|
|
1051
|
+
fallbackNeeded = true;
|
|
973
1052
|
}
|
|
974
1053
|
}
|
|
975
1054
|
let handle;
|
|
976
|
-
|
|
1055
|
+
let info;
|
|
1056
|
+
if (!fallbackNeeded) try {
|
|
977
1057
|
handle = await (0, node_fs_promises.open)(resolvedPath, "r");
|
|
978
1058
|
} catch {
|
|
979
|
-
|
|
1059
|
+
fallbackNeeded = true;
|
|
980
1060
|
}
|
|
981
|
-
|
|
982
|
-
try {
|
|
1061
|
+
if (handle !== void 0) try {
|
|
983
1062
|
info = await handle.stat();
|
|
984
1063
|
} catch {
|
|
985
1064
|
await handle.close();
|
|
986
|
-
|
|
1065
|
+
handle = void 0;
|
|
1066
|
+
fallbackNeeded = true;
|
|
987
1067
|
}
|
|
988
|
-
if (!info.isFile()) {
|
|
1068
|
+
if (handle !== void 0 && info !== void 0 && !info.isFile()) {
|
|
989
1069
|
await handle.close();
|
|
990
|
-
|
|
1070
|
+
handle = void 0;
|
|
1071
|
+
info = void 0;
|
|
1072
|
+
fallbackNeeded = true;
|
|
1073
|
+
}
|
|
1074
|
+
if (fallbackNeeded) {
|
|
1075
|
+
const shellPath = resolveStaticFallbackPath(root, index, fallback?.exclude ?? "/api", context.method, context.url.pathname, request.headers.get("accept") ?? "");
|
|
1076
|
+
if (fallback === void 0 || shellPath === void 0) return next();
|
|
1077
|
+
let shellReal;
|
|
1078
|
+
try {
|
|
1079
|
+
if (canonicalRootPromise === void 0) canonicalRootPromise = (0, node_fs_promises.realpath)(root);
|
|
1080
|
+
const [rootReal, candidate] = await Promise.all([canonicalRootPromise, (0, node_fs_promises.realpath)(shellPath)]);
|
|
1081
|
+
if (!isContainedPath(candidate, rootReal)) return next();
|
|
1082
|
+
shellReal = candidate;
|
|
1083
|
+
} catch {
|
|
1084
|
+
return next();
|
|
1085
|
+
}
|
|
1086
|
+
let shellHandle;
|
|
1087
|
+
try {
|
|
1088
|
+
shellHandle = await (0, node_fs_promises.open)(shellReal, "r");
|
|
1089
|
+
} catch {
|
|
1090
|
+
return next();
|
|
1091
|
+
}
|
|
1092
|
+
try {
|
|
1093
|
+
const body = streamFile(shellHandle);
|
|
1094
|
+
return new Response(body, {
|
|
1095
|
+
status: 200,
|
|
1096
|
+
headers: new Headers({ "content-type": lookupContentType(shellReal) })
|
|
1097
|
+
});
|
|
1098
|
+
} catch (error) {
|
|
1099
|
+
await shellHandle.close().catch(() => {});
|
|
1100
|
+
throw error;
|
|
1101
|
+
}
|
|
991
1102
|
}
|
|
1103
|
+
if (handle === void 0 || info === void 0) return next();
|
|
992
1104
|
let streaming = false;
|
|
993
1105
|
try {
|
|
994
1106
|
const headers = new Headers({
|
|
@@ -1050,32 +1162,6 @@ function createStatic(options) {
|
|
|
1050
1162
|
if (!streaming) await handle.close().catch(() => {});
|
|
1051
1163
|
throw error;
|
|
1052
1164
|
}
|
|
1053
|
-
function trySpaFallback() {
|
|
1054
|
-
if (fallback === void 0) return next();
|
|
1055
|
-
if (context.method !== "GET") return next();
|
|
1056
|
-
if ((0, node_path.extname)(context.url.pathname) !== "") return next();
|
|
1057
|
-
const accept = request.headers.get("accept") ?? "";
|
|
1058
|
-
if (!accept.includes("text/html") && !accept.includes("*/*")) return next();
|
|
1059
|
-
if (isUnderPath(context.url.pathname, fallback.exclude)) return next();
|
|
1060
|
-
const shellPath = (0, node_path.join)(root, index);
|
|
1061
|
-
return Promise.all([canonicalRoot(), (0, node_fs_promises.realpath)(shellPath)]).then(([rootReal, shellReal]) => {
|
|
1062
|
-
if (!isContainedPath(shellReal, rootReal)) return next();
|
|
1063
|
-
return (0, node_fs_promises.open)(shellReal, "r").then((shellHandle) => {
|
|
1064
|
-
let shellStreaming = false;
|
|
1065
|
-
try {
|
|
1066
|
-
const body = streamFile(shellHandle);
|
|
1067
|
-
shellStreaming = true;
|
|
1068
|
-
return new Response(body, {
|
|
1069
|
-
status: 200,
|
|
1070
|
-
headers: new Headers({ "content-type": lookupContentType(shellReal) })
|
|
1071
|
-
});
|
|
1072
|
-
} catch (error) {
|
|
1073
|
-
if (!shellStreaming) shellHandle.close().catch(() => {});
|
|
1074
|
-
throw error;
|
|
1075
|
-
}
|
|
1076
|
-
});
|
|
1077
|
-
}).catch(() => next());
|
|
1078
|
-
}
|
|
1079
1165
|
};
|
|
1080
1166
|
}
|
|
1081
1167
|
/**
|
|
@@ -1119,7 +1205,7 @@ function createMultipart(options = {}) {
|
|
|
1119
1205
|
throw error;
|
|
1120
1206
|
}
|
|
1121
1207
|
if (body === void 0) return next();
|
|
1122
|
-
context.state
|
|
1208
|
+
Object.assign(context.state, { multipart: body });
|
|
1123
1209
|
try {
|
|
1124
1210
|
return await next();
|
|
1125
1211
|
} catch (error) {
|
|
@@ -1164,14 +1250,13 @@ function createCompression(options) {
|
|
|
1164
1250
|
const threshold = options?.threshold ?? _src_core.DEFAULT_COMPRESSION_THRESHOLD;
|
|
1165
1251
|
const filter = options?.filter;
|
|
1166
1252
|
const encodings = ["gzip", "deflate"];
|
|
1167
|
-
const gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
1168
|
-
const deflateAsync = (0, node_util.promisify)(node_zlib.deflate);
|
|
1169
1253
|
return async (request, context, next) => {
|
|
1170
|
-
|
|
1254
|
+
const response = await next();
|
|
1255
|
+
return (0, _src_core.compressResponse)(request, context, response, {
|
|
1171
1256
|
threshold,
|
|
1172
|
-
filter,
|
|
1257
|
+
...filter !== void 0 ? { filter } : {},
|
|
1173
1258
|
encodings,
|
|
1174
|
-
compress:
|
|
1259
|
+
compress: compressNodeBytes
|
|
1175
1260
|
});
|
|
1176
1261
|
};
|
|
1177
1262
|
}
|
|
@@ -1190,6 +1275,7 @@ exports.MULTIPART_MAX_PREAMBLE = MULTIPART_MAX_PREAMBLE;
|
|
|
1190
1275
|
exports.MULTIPART_REASON_STATUS = MULTIPART_REASON_STATUS;
|
|
1191
1276
|
exports.MultipartError = MultipartError;
|
|
1192
1277
|
exports.RESERVED_DEVICE_NAMES = RESERVED_DEVICE_NAMES;
|
|
1278
|
+
exports.compressNodeBytes = compressNodeBytes;
|
|
1193
1279
|
exports.computeFileETag = computeFileETag;
|
|
1194
1280
|
exports.createCompression = createCompression;
|
|
1195
1281
|
exports.createMultipart = createMultipart;
|
|
@@ -1202,6 +1288,7 @@ exports.isMultipartError = isMultipartError;
|
|
|
1202
1288
|
exports.isReservedDeviceName = isReservedDeviceName;
|
|
1203
1289
|
exports.isUnderPath = isUnderPath;
|
|
1204
1290
|
exports.lookupContentType = lookupContentType;
|
|
1291
|
+
exports.matchesBytes = matchesBytes;
|
|
1205
1292
|
exports.moveUploadedFile = moveUploadedFile;
|
|
1206
1293
|
exports.multipartBoundary = multipartBoundary;
|
|
1207
1294
|
exports.parseMultipartRequest = parseMultipartRequest;
|
|
@@ -1209,6 +1296,7 @@ exports.parsePartHeaders = parsePartHeaders;
|
|
|
1209
1296
|
exports.readUploadedFile = readUploadedFile;
|
|
1210
1297
|
exports.resolveDefaultDirectory = resolveDefaultDirectory;
|
|
1211
1298
|
exports.resolveMultipartLimits = resolveMultipartLimits;
|
|
1299
|
+
exports.resolveStaticFallbackPath = resolveStaticFallbackPath;
|
|
1212
1300
|
exports.resolveStaticPath = resolveStaticPath;
|
|
1213
1301
|
exports.streamFile = streamFile;
|
|
1214
1302
|
exports.streamUploadedFile = streamUploadedFile;
|