@abejarano/ts-express-server 1.7.3 → 1.7.5
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.
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type fileUpload from "express-fileupload";
|
|
1
2
|
export declare enum ServerRuntime {
|
|
2
3
|
Express = "express",
|
|
3
4
|
Bun = "bun"
|
|
@@ -12,7 +13,7 @@ export interface ServerRequest {
|
|
|
12
13
|
query: Record<string, string | string[]>;
|
|
13
14
|
headers: Record<string, string | string[] | undefined>;
|
|
14
15
|
body?: unknown;
|
|
15
|
-
files?:
|
|
16
|
+
files?: ServerFiles;
|
|
16
17
|
cookies?: Record<string, string>;
|
|
17
18
|
ip?: string;
|
|
18
19
|
requestId?: string;
|
|
@@ -33,8 +34,21 @@ export interface ServerResponse {
|
|
|
33
34
|
secure?: boolean;
|
|
34
35
|
sameSite?: "lax" | "strict" | "none";
|
|
35
36
|
}): this;
|
|
37
|
+
download?(path: string, filename?: string, callback?: (err?: unknown) => void): this;
|
|
36
38
|
end(body?: unknown): void | Promise<void>;
|
|
37
39
|
}
|
|
40
|
+
export type BunMultipartFile = {
|
|
41
|
+
name: string;
|
|
42
|
+
size: number;
|
|
43
|
+
type: string;
|
|
44
|
+
slice?: (start?: number, end?: number) => {
|
|
45
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
46
|
+
};
|
|
47
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
48
|
+
lastModified?: number;
|
|
49
|
+
};
|
|
50
|
+
export type ServerFile = BunMultipartFile | fileUpload.UploadedFile;
|
|
51
|
+
export type ServerFiles = Record<string, ServerFile | ServerFile[]>;
|
|
38
52
|
export type ServerHandlerInput = ServerHandler | ServerHandler[] | ServerRouter;
|
|
39
53
|
export interface ServerRouter {
|
|
40
54
|
use(pathOrHandler: string | ServerHandlerInput, ...handlers: ServerHandlerInput[]): void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ServerAdapter, ServerApp, ServerInstance, ServerRequest, ServerRouter, ServerRuntime } from "../abstract/ServerTypes";
|
|
1
|
+
import { ServerAdapter, ServerApp, ServerInstance, ServerRequest, ServerRouter, ServerRuntime, BunMultipartFile } from "../abstract/ServerTypes";
|
|
2
2
|
export declare class BunAdapter implements ServerAdapter {
|
|
3
3
|
runtime: ServerRuntime;
|
|
4
4
|
createApp(): ServerApp;
|
|
@@ -6,13 +6,7 @@ export declare class BunAdapter implements ServerAdapter {
|
|
|
6
6
|
configure(app: ServerApp, _port: number): void;
|
|
7
7
|
listen(app: ServerApp, port: number, onListen: () => void): ServerInstance;
|
|
8
8
|
}
|
|
9
|
-
type MultipartFile =
|
|
10
|
-
name: string;
|
|
11
|
-
size: number;
|
|
12
|
-
type: string;
|
|
13
|
-
arrayBuffer(): Promise<ArrayBuffer>;
|
|
14
|
-
lastModified?: number;
|
|
15
|
-
};
|
|
9
|
+
type MultipartFile = BunMultipartFile;
|
|
16
10
|
export declare function getFiles(req: ServerRequest, field: string): MultipartFile[];
|
|
17
11
|
export declare function getFile(req: ServerRequest, field: string): MultipartFile | undefined;
|
|
18
12
|
export {};
|
|
@@ -5,7 +5,7 @@ exports.getFiles = getFiles;
|
|
|
5
5
|
exports.getFile = getFile;
|
|
6
6
|
const ServerTypes_1 = require("../abstract/ServerTypes");
|
|
7
7
|
class BunResponse {
|
|
8
|
-
constructor(cookieJar, handlerTimeoutMs) {
|
|
8
|
+
constructor(cookieJar, handlerTimeoutMs, cookieDefaults) {
|
|
9
9
|
this.statusCode = 200;
|
|
10
10
|
this.statusExplicitlySet = false;
|
|
11
11
|
this.headers = new Headers();
|
|
@@ -14,16 +14,26 @@ class BunResponse {
|
|
|
14
14
|
this.ended = false;
|
|
15
15
|
this.cookieJar = cookieJar;
|
|
16
16
|
this.handlerTimeoutMs = handlerTimeoutMs;
|
|
17
|
+
this.cookieDefaults = cookieDefaults;
|
|
17
18
|
this.endPromise = new Promise((resolve) => {
|
|
18
19
|
this.resolveEnd = resolve;
|
|
19
20
|
});
|
|
20
21
|
}
|
|
21
22
|
status(code) {
|
|
23
|
+
if (this.ended) {
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
22
26
|
this.statusExplicitlySet = true;
|
|
23
27
|
this.statusCode = code;
|
|
24
28
|
return this;
|
|
25
29
|
}
|
|
26
30
|
set(name, value) {
|
|
31
|
+
if (this.ended) {
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
if (hasInvalidHeaderValue(value)) {
|
|
35
|
+
return this;
|
|
36
|
+
}
|
|
27
37
|
if (name.toLowerCase() === "set-cookie") {
|
|
28
38
|
this.setCookies.push(value);
|
|
29
39
|
return this;
|
|
@@ -35,14 +45,24 @@ class BunResponse {
|
|
|
35
45
|
return this.set(name, value);
|
|
36
46
|
}
|
|
37
47
|
cookie(name, value, options = {}) {
|
|
48
|
+
if (this.ended) {
|
|
49
|
+
return this;
|
|
50
|
+
}
|
|
51
|
+
if (!isValidCookieName(name)) {
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
const resolvedOptions = applyCookieDefaults(options, this.cookieDefaults);
|
|
38
55
|
if (this.cookieJar && typeof this.cookieJar.set === "function") {
|
|
39
|
-
this.cookieJar.set(name, value, toCookieJarOptions(
|
|
56
|
+
this.cookieJar.set(name, value, toCookieJarOptions(resolvedOptions));
|
|
40
57
|
return this;
|
|
41
58
|
}
|
|
42
|
-
this.setCookies.push(serializeCookie(name, value,
|
|
59
|
+
this.setCookies.push(serializeCookie(name, value, resolvedOptions));
|
|
43
60
|
return this;
|
|
44
61
|
}
|
|
45
62
|
json(body) {
|
|
63
|
+
if (this.ended) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
46
66
|
if (!this.headers.has("content-type")) {
|
|
47
67
|
this.headers.set("content-type", "application/json");
|
|
48
68
|
}
|
|
@@ -51,6 +71,9 @@ class BunResponse {
|
|
|
51
71
|
this.resolveEnd?.();
|
|
52
72
|
}
|
|
53
73
|
send(body) {
|
|
74
|
+
if (this.ended) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
54
77
|
if (body instanceof Response) {
|
|
55
78
|
this.rawResponse = body;
|
|
56
79
|
this.ended = true;
|
|
@@ -77,6 +100,9 @@ class BunResponse {
|
|
|
77
100
|
this.resolveEnd?.();
|
|
78
101
|
}
|
|
79
102
|
end(body) {
|
|
103
|
+
if (this.ended) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
80
106
|
if (body !== undefined) {
|
|
81
107
|
this.send(body);
|
|
82
108
|
return;
|
|
@@ -195,10 +221,11 @@ class BunRouter {
|
|
|
195
221
|
}, timeoutMs);
|
|
196
222
|
})
|
|
197
223
|
: null;
|
|
224
|
+
const runPromise = runHandlers(handlers, req, res);
|
|
198
225
|
try {
|
|
199
226
|
chainCompleted = timeoutPromise
|
|
200
|
-
? await Promise.race([
|
|
201
|
-
: await
|
|
227
|
+
? await Promise.race([runPromise, timeoutPromise])
|
|
228
|
+
: await runPromise;
|
|
202
229
|
}
|
|
203
230
|
catch (error) {
|
|
204
231
|
if (!res.isEnded()) {
|
|
@@ -213,6 +240,7 @@ class BunRouter {
|
|
|
213
240
|
}
|
|
214
241
|
}
|
|
215
242
|
if (timedOut) {
|
|
243
|
+
runPromise.catch(() => { });
|
|
216
244
|
done();
|
|
217
245
|
return;
|
|
218
246
|
}
|
|
@@ -278,6 +306,7 @@ class BunApp extends BunRouter {
|
|
|
278
306
|
constructor() {
|
|
279
307
|
super(...arguments);
|
|
280
308
|
this.settings = new Map();
|
|
309
|
+
this.activeRequests = 0;
|
|
281
310
|
}
|
|
282
311
|
set(key, value) {
|
|
283
312
|
this.settings.set(key, value);
|
|
@@ -289,12 +318,29 @@ class BunApp extends BunRouter {
|
|
|
289
318
|
return async (request, server) => {
|
|
290
319
|
const client = server?.requestIP?.(request);
|
|
291
320
|
const cookieJar = request.cookies;
|
|
292
|
-
const
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
321
|
+
const handlerTimeoutSetting = this.get("handlerTimeoutMs");
|
|
322
|
+
const handlerTimeoutMs = handlerTimeoutSetting === undefined
|
|
323
|
+
? DEFAULT_HANDLER_TIMEOUT_MS
|
|
324
|
+
: typeof handlerTimeoutSetting === "number"
|
|
325
|
+
? handlerTimeoutSetting
|
|
326
|
+
: undefined;
|
|
327
|
+
const trustProxy = resolveTrustProxySetting(this.get("trustProxy"));
|
|
328
|
+
const maxConcurrentRequests = Number(this.get("maxConcurrentRequests") ?? 0);
|
|
329
|
+
if (maxConcurrentRequests > 0 && this.activeRequests >= maxConcurrentRequests) {
|
|
330
|
+
return new Response(JSON.stringify({ message: "Server busy" }), {
|
|
331
|
+
status: 503,
|
|
332
|
+
headers: [["content-type", "application/json"]],
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
this.activeRequests += 1;
|
|
336
|
+
const req = createRequest(request, client?.address, trustProxy);
|
|
337
|
+
const res = new BunResponse(cookieJar, handlerTimeoutMs, resolveCookieDefaults(this.get("cookieDefaults")));
|
|
338
|
+
try {
|
|
339
|
+
await this.handle(req, res, () => undefined);
|
|
340
|
+
}
|
|
341
|
+
finally {
|
|
342
|
+
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
343
|
+
}
|
|
298
344
|
if (!res.isEnded()) {
|
|
299
345
|
res.status(204).end();
|
|
300
346
|
}
|
|
@@ -314,6 +360,9 @@ class BunAdapter {
|
|
|
314
360
|
}
|
|
315
361
|
configure(app, _port) {
|
|
316
362
|
const bunApp = app;
|
|
363
|
+
if (shouldEnableSecurityHeaders(bunApp.get("securityHeaders"))) {
|
|
364
|
+
bunApp.use(createSecurityHeadersMiddleware(bunApp));
|
|
365
|
+
}
|
|
317
366
|
bunApp.use(createMultipartBodyParser(bunApp));
|
|
318
367
|
bunApp.use(createJsonBodyParser(bunApp));
|
|
319
368
|
bunApp.use(createUrlEncodedBodyParser(bunApp));
|
|
@@ -336,7 +385,7 @@ class BunAdapter {
|
|
|
336
385
|
exports.BunAdapter = BunAdapter;
|
|
337
386
|
const createJsonBodyParser = (app) => {
|
|
338
387
|
return async (req, res, next) => {
|
|
339
|
-
if (!req.raw || req.body !== undefined) {
|
|
388
|
+
if (!req.raw || req.body !== undefined || req.files !== undefined) {
|
|
340
389
|
return next();
|
|
341
390
|
}
|
|
342
391
|
const method = String(req.method || "").toUpperCase();
|
|
@@ -357,9 +406,19 @@ const createJsonBodyParser = (app) => {
|
|
|
357
406
|
return;
|
|
358
407
|
}
|
|
359
408
|
try {
|
|
360
|
-
|
|
409
|
+
if (contentLength === undefined) {
|
|
410
|
+
const text = await readBodyTextWithLimit(req.raw, limit);
|
|
411
|
+
req.body = JSON.parse(text);
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
req.body = await req.raw.json();
|
|
415
|
+
}
|
|
361
416
|
}
|
|
362
|
-
catch {
|
|
417
|
+
catch (error) {
|
|
418
|
+
if (error?.status === 413) {
|
|
419
|
+
res.status(413).json({ message: "Payload too large" });
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
363
422
|
res.status(400).json({ message: "Invalid JSON" });
|
|
364
423
|
return;
|
|
365
424
|
}
|
|
@@ -371,6 +430,10 @@ const createUrlEncodedBodyParser = (app) => {
|
|
|
371
430
|
if (!req.raw || req.body !== undefined) {
|
|
372
431
|
return next();
|
|
373
432
|
}
|
|
433
|
+
const method = String(req.method || "").toUpperCase();
|
|
434
|
+
if (method === "GET" || method === "HEAD") {
|
|
435
|
+
return next();
|
|
436
|
+
}
|
|
374
437
|
const contentType = String(req.headers["content-type"] || "");
|
|
375
438
|
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
376
439
|
return next();
|
|
@@ -381,14 +444,26 @@ const createUrlEncodedBodyParser = (app) => {
|
|
|
381
444
|
res.status(413).json({ message: "Payload too large" });
|
|
382
445
|
return;
|
|
383
446
|
}
|
|
384
|
-
|
|
385
|
-
|
|
447
|
+
try {
|
|
448
|
+
const text = contentLength === undefined
|
|
449
|
+
? await readBodyTextWithLimit(req.raw, limit)
|
|
450
|
+
: await req.raw.text();
|
|
451
|
+
req.body = Object.fromEntries(new URLSearchParams(text));
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
if (error?.status === 413) {
|
|
455
|
+
res.status(413).json({ message: "Payload too large" });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
res.status(400).json({ message: "Invalid form data" });
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
386
461
|
next();
|
|
387
462
|
};
|
|
388
463
|
};
|
|
389
464
|
const createMultipartBodyParser = (app) => {
|
|
390
465
|
return async (req, res, next) => {
|
|
391
|
-
if (!req.raw || req.body !== undefined) {
|
|
466
|
+
if (!req.raw || req.body !== undefined || req.files !== undefined) {
|
|
392
467
|
return next();
|
|
393
468
|
}
|
|
394
469
|
const contentType = String(req.headers["content-type"] || "");
|
|
@@ -398,6 +473,11 @@ const createMultipartBodyParser = (app) => {
|
|
|
398
473
|
const options = normalizeMultipartOptions(app.get("multipart"));
|
|
399
474
|
const lengthHeader = req.headers["content-length"];
|
|
400
475
|
const contentLength = parseContentLength(lengthHeader);
|
|
476
|
+
// If content-length is missing, body size limits can't be enforced pre-read.
|
|
477
|
+
if (contentLength === undefined) {
|
|
478
|
+
res.status(411).json({ message: "Length required" });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
401
481
|
if (contentLength !== undefined && contentLength > options.maxBodyBytes) {
|
|
402
482
|
res.status(413).json({ message: "Payload too large" });
|
|
403
483
|
return;
|
|
@@ -417,17 +497,34 @@ const createMultipartBodyParser = (app) => {
|
|
|
417
497
|
res.status(415).json({ message: "Unsupported media type" });
|
|
418
498
|
return;
|
|
419
499
|
}
|
|
500
|
+
if (options.validateFile) {
|
|
501
|
+
const isValid = await options.validateFile(value);
|
|
502
|
+
if (!isValid) {
|
|
503
|
+
res.status(415).json({ message: "Unsupported media type" });
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
if (options.allowedFileSignatures) {
|
|
508
|
+
const signatureAllowed = await isAllowedFileSignature(value, options.allowedFileSignatures);
|
|
509
|
+
if (!signatureAllowed) {
|
|
510
|
+
res.status(415).json({ message: "Unsupported media type" });
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
420
514
|
fileCount += 1;
|
|
421
515
|
if (fileCount > options.maxFiles) {
|
|
422
516
|
res.status(413).json({ message: "Payload too large" });
|
|
423
517
|
return;
|
|
424
518
|
}
|
|
425
|
-
const
|
|
426
|
-
if (
|
|
427
|
-
|
|
519
|
+
const existing = files[key];
|
|
520
|
+
if (!existing) {
|
|
521
|
+
files[key] = value;
|
|
522
|
+
}
|
|
523
|
+
else if (Array.isArray(existing)) {
|
|
524
|
+
existing.push(value);
|
|
428
525
|
}
|
|
429
526
|
else {
|
|
430
|
-
files[key] = [value];
|
|
527
|
+
files[key] = [existing, value];
|
|
431
528
|
}
|
|
432
529
|
continue;
|
|
433
530
|
}
|
|
@@ -457,10 +554,11 @@ const createMultipartBodyParser = (app) => {
|
|
|
457
554
|
next();
|
|
458
555
|
};
|
|
459
556
|
};
|
|
460
|
-
function createRequest(request,
|
|
557
|
+
function createRequest(request, remoteAddress, trustProxy) {
|
|
461
558
|
const url = new URL(request.url);
|
|
462
559
|
const headers = toHeaderRecord(request.headers);
|
|
463
560
|
const query = toQueryRecord(url.searchParams);
|
|
561
|
+
const ip = resolveClientIp(headers, remoteAddress, trustProxy);
|
|
464
562
|
return {
|
|
465
563
|
method: request.method.toUpperCase(),
|
|
466
564
|
path: normalizePath(url.pathname),
|
|
@@ -469,7 +567,7 @@ function createRequest(request, ipOverride) {
|
|
|
469
567
|
query,
|
|
470
568
|
headers,
|
|
471
569
|
cookies: parseCookies(headers.cookie),
|
|
472
|
-
ip
|
|
570
|
+
ip,
|
|
473
571
|
raw: request,
|
|
474
572
|
};
|
|
475
573
|
}
|
|
@@ -557,6 +655,10 @@ function toCookieJarOptions(options) {
|
|
|
557
655
|
};
|
|
558
656
|
}
|
|
559
657
|
function getBodyLimit(app) {
|
|
658
|
+
const configured = app.get("bodyLimit");
|
|
659
|
+
if (typeof configured === "number" && Number.isFinite(configured)) {
|
|
660
|
+
return configured;
|
|
661
|
+
}
|
|
560
662
|
return normalizeMultipartOptions(app.get("multipart")).maxBodyBytes;
|
|
561
663
|
}
|
|
562
664
|
function normalizeMultipartOptions(input) {
|
|
@@ -569,6 +671,8 @@ function normalizeMultipartOptions(input) {
|
|
|
569
671
|
maxFileBytes: value.maxFileBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFileBytes,
|
|
570
672
|
maxFiles: value.maxFiles ?? DEFAULT_MULTIPART_OPTIONS.maxFiles,
|
|
571
673
|
allowedMimeTypes: value.allowedMimeTypes,
|
|
674
|
+
allowedFileSignatures: value.allowedFileSignatures,
|
|
675
|
+
validateFile: value.validateFile,
|
|
572
676
|
};
|
|
573
677
|
}
|
|
574
678
|
function isMimeAllowed(type, allowed) {
|
|
@@ -629,9 +733,12 @@ function parseContentLength(header) {
|
|
|
629
733
|
const parsed = Number.parseInt(value, 10);
|
|
630
734
|
return Number.isNaN(parsed) ? undefined : parsed;
|
|
631
735
|
}
|
|
736
|
+
const DEFAULT_HANDLER_TIMEOUT_MS = 30000;
|
|
632
737
|
function readSetCookieHeaders(headers) {
|
|
633
738
|
const bunHeaders = headers;
|
|
634
|
-
const setCookieFromApi = bunHeaders.getSetCookie?.() ??
|
|
739
|
+
const setCookieFromApi = bunHeaders.getSetCookie?.() ??
|
|
740
|
+
bunHeaders.getAll?.("set-cookie") ??
|
|
741
|
+
bunHeaders.getAll?.("Set-Cookie");
|
|
635
742
|
if (setCookieFromApi && setCookieFromApi.length > 0) {
|
|
636
743
|
return setCookieFromApi;
|
|
637
744
|
}
|
|
@@ -645,7 +752,11 @@ function readSetCookieHeaders(headers) {
|
|
|
645
752
|
}
|
|
646
753
|
function getFiles(req, field) {
|
|
647
754
|
const map = (req.files ?? {});
|
|
648
|
-
|
|
755
|
+
const entry = map[field];
|
|
756
|
+
if (!entry) {
|
|
757
|
+
return [];
|
|
758
|
+
}
|
|
759
|
+
return Array.isArray(entry) ? entry : [entry];
|
|
649
760
|
}
|
|
650
761
|
function getFile(req, field) {
|
|
651
762
|
return getFiles(req, field)[0];
|
|
@@ -657,6 +768,78 @@ function extractIp(headers) {
|
|
|
657
768
|
}
|
|
658
769
|
return headers["x-real-ip"];
|
|
659
770
|
}
|
|
771
|
+
function resolveClientIp(headers, remoteAddress, trustProxy) {
|
|
772
|
+
const normalizedRemote = normalizeIp(remoteAddress);
|
|
773
|
+
if (!trustProxy) {
|
|
774
|
+
return normalizedRemote;
|
|
775
|
+
}
|
|
776
|
+
if (trustProxy === true) {
|
|
777
|
+
return extractIp(headers) || normalizedRemote;
|
|
778
|
+
}
|
|
779
|
+
if (typeof trustProxy === "function") {
|
|
780
|
+
return trustProxy(normalizedRemote)
|
|
781
|
+
? extractIp(headers) || normalizedRemote
|
|
782
|
+
: normalizedRemote;
|
|
783
|
+
}
|
|
784
|
+
const trusted = isTrustedProxy(normalizedRemote, trustProxy);
|
|
785
|
+
return trusted ? extractIp(headers) || normalizedRemote : normalizedRemote;
|
|
786
|
+
}
|
|
787
|
+
function normalizeIp(ip) {
|
|
788
|
+
if (!ip) {
|
|
789
|
+
return undefined;
|
|
790
|
+
}
|
|
791
|
+
if (ip.includes(".") && ip.includes(":")) {
|
|
792
|
+
return ip.split(":")[0];
|
|
793
|
+
}
|
|
794
|
+
return ip;
|
|
795
|
+
}
|
|
796
|
+
function isTrustedProxy(ip, allowlist) {
|
|
797
|
+
if (!ip) {
|
|
798
|
+
return false;
|
|
799
|
+
}
|
|
800
|
+
for (const entry of allowlist) {
|
|
801
|
+
if (entry.includes("/")) {
|
|
802
|
+
if (matchesCidr(ip, entry)) {
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (entry === ip) {
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
function matchesCidr(ip, cidr) {
|
|
814
|
+
const [range, bitsString] = cidr.split("/");
|
|
815
|
+
if (!range || !bitsString) {
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
if (range.includes(":") || ip.includes(":")) {
|
|
819
|
+
return range === ip;
|
|
820
|
+
}
|
|
821
|
+
const bits = Number(bitsString);
|
|
822
|
+
if (!Number.isFinite(bits) || bits < 0 || bits > 32) {
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
const ipValue = ipv4ToInt(ip);
|
|
826
|
+
const rangeValue = ipv4ToInt(range);
|
|
827
|
+
if (ipValue === null || rangeValue === null) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const mask = bits === 0 ? 0 : ~((1 << (32 - bits)) - 1);
|
|
831
|
+
return (ipValue & mask) === (rangeValue & mask);
|
|
832
|
+
}
|
|
833
|
+
function ipv4ToInt(ip) {
|
|
834
|
+
const parts = ip.split(".").map((part) => Number(part));
|
|
835
|
+
if (parts.length !== 4 || parts.some((part) => part < 0 || part > 255)) {
|
|
836
|
+
return null;
|
|
837
|
+
}
|
|
838
|
+
return ((parts[0] << 24) +
|
|
839
|
+
(parts[1] << 16) +
|
|
840
|
+
(parts[2] << 8) +
|
|
841
|
+
parts[3]) >>> 0;
|
|
842
|
+
}
|
|
660
843
|
function normalizePath(path) {
|
|
661
844
|
if (!path.startsWith("/")) {
|
|
662
845
|
path = `/${path}`;
|
|
@@ -689,7 +872,12 @@ function matchPath(routePath, requestPath) {
|
|
|
689
872
|
const routePart = routeParts[index];
|
|
690
873
|
const requestPart = requestParts[index];
|
|
691
874
|
if (routePart.startsWith(":")) {
|
|
692
|
-
|
|
875
|
+
try {
|
|
876
|
+
params[routePart.slice(1)] = decodeURIComponent(requestPart);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
693
881
|
continue;
|
|
694
882
|
}
|
|
695
883
|
if (routePart !== requestPart) {
|
|
@@ -709,6 +897,128 @@ function normalizeHandlers(inputs) {
|
|
|
709
897
|
}
|
|
710
898
|
return handlers;
|
|
711
899
|
}
|
|
900
|
+
function applyCookieDefaults(options, defaults) {
|
|
901
|
+
if (!defaults) {
|
|
902
|
+
return options;
|
|
903
|
+
}
|
|
904
|
+
const applyTo = defaults.applyTo ?? "session";
|
|
905
|
+
const isSession = options.maxAge === undefined && options.expires === undefined;
|
|
906
|
+
if (applyTo !== "all" && !isSession) {
|
|
907
|
+
return options;
|
|
908
|
+
}
|
|
909
|
+
return { ...defaults.options, ...options };
|
|
910
|
+
}
|
|
911
|
+
function resolveCookieDefaults(input) {
|
|
912
|
+
if (!input || typeof input !== "object") {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
const defaults = input;
|
|
916
|
+
if (!defaults.options || typeof defaults.options !== "object") {
|
|
917
|
+
return undefined;
|
|
918
|
+
}
|
|
919
|
+
return defaults;
|
|
920
|
+
}
|
|
921
|
+
function resolveTrustProxySetting(input) {
|
|
922
|
+
if (input === true || input === false) {
|
|
923
|
+
return input;
|
|
924
|
+
}
|
|
925
|
+
if (Array.isArray(input) && input.every((entry) => typeof entry === "string")) {
|
|
926
|
+
return input;
|
|
927
|
+
}
|
|
928
|
+
if (typeof input === "function") {
|
|
929
|
+
return input;
|
|
930
|
+
}
|
|
931
|
+
return undefined;
|
|
932
|
+
}
|
|
933
|
+
function hasInvalidHeaderValue(value) {
|
|
934
|
+
return value.includes("\r") || value.includes("\n");
|
|
935
|
+
}
|
|
936
|
+
function shouldEnableSecurityHeaders(setting) {
|
|
937
|
+
if (typeof setting === "boolean") {
|
|
938
|
+
return setting;
|
|
939
|
+
}
|
|
940
|
+
return process?.env?.NODE_ENV === "production";
|
|
941
|
+
}
|
|
942
|
+
function createSecurityHeadersMiddleware(_app) {
|
|
943
|
+
return (_req, res, next) => {
|
|
944
|
+
res.set("x-content-type-options", "nosniff");
|
|
945
|
+
res.set("referrer-policy", "no-referrer");
|
|
946
|
+
res.set("x-frame-options", "DENY");
|
|
947
|
+
next();
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
async function readBodyTextWithLimit(request, limit) {
|
|
951
|
+
const bytes = await readBodyBytesWithLimit(request, limit);
|
|
952
|
+
return new TextDecoder().decode(bytes);
|
|
953
|
+
}
|
|
954
|
+
async function readBodyBytesWithLimit(request, limit) {
|
|
955
|
+
if (!request.body) {
|
|
956
|
+
return new Uint8Array();
|
|
957
|
+
}
|
|
958
|
+
const reader = request.body.getReader();
|
|
959
|
+
const chunks = [];
|
|
960
|
+
let total = 0;
|
|
961
|
+
while (true) {
|
|
962
|
+
const { done, value } = await reader.read();
|
|
963
|
+
if (done) {
|
|
964
|
+
break;
|
|
965
|
+
}
|
|
966
|
+
if (value) {
|
|
967
|
+
total += value.length;
|
|
968
|
+
if (total > limit) {
|
|
969
|
+
const error = new Error("Payload too large");
|
|
970
|
+
error.status = 413;
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
chunks.push(value);
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const result = new Uint8Array(total);
|
|
977
|
+
let offset = 0;
|
|
978
|
+
for (const chunk of chunks) {
|
|
979
|
+
result.set(chunk, offset);
|
|
980
|
+
offset += chunk.length;
|
|
981
|
+
}
|
|
982
|
+
return result;
|
|
983
|
+
}
|
|
984
|
+
async function isAllowedFileSignature(file, allowed) {
|
|
985
|
+
const head = typeof file.slice === "function" ? file.slice(0, 32) : file;
|
|
986
|
+
const buffer = new Uint8Array(await head.arrayBuffer());
|
|
987
|
+
for (const kind of allowed) {
|
|
988
|
+
if (matchesSignature(buffer, kind)) {
|
|
989
|
+
return true;
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
return false;
|
|
993
|
+
}
|
|
994
|
+
function matchesSignature(buffer, kind) {
|
|
995
|
+
if (kind === "png") {
|
|
996
|
+
return (buffer.length >= 8 &&
|
|
997
|
+
buffer[0] === 0x89 &&
|
|
998
|
+
buffer[1] === 0x50 &&
|
|
999
|
+
buffer[2] === 0x4e &&
|
|
1000
|
+
buffer[3] === 0x47 &&
|
|
1001
|
+
buffer[4] === 0x0d &&
|
|
1002
|
+
buffer[5] === 0x0a &&
|
|
1003
|
+
buffer[6] === 0x1a &&
|
|
1004
|
+
buffer[7] === 0x0a);
|
|
1005
|
+
}
|
|
1006
|
+
if (kind === "pdf") {
|
|
1007
|
+
return (buffer.length >= 4 &&
|
|
1008
|
+
buffer[0] === 0x25 &&
|
|
1009
|
+
buffer[1] === 0x50 &&
|
|
1010
|
+
buffer[2] === 0x44 &&
|
|
1011
|
+
buffer[3] === 0x46);
|
|
1012
|
+
}
|
|
1013
|
+
// jpg/jpeg
|
|
1014
|
+
return (buffer.length >= 3 &&
|
|
1015
|
+
buffer[0] === 0xff &&
|
|
1016
|
+
buffer[1] === 0xd8 &&
|
|
1017
|
+
buffer[2] === 0xff);
|
|
1018
|
+
}
|
|
1019
|
+
function isValidCookieName(name) {
|
|
1020
|
+
return /^[!#$%&'*+\-.^_|~0-9A-Za-z]+$/.test(name);
|
|
1021
|
+
}
|
|
712
1022
|
async function runHandlers(handlers, req, res) {
|
|
713
1023
|
let index = 0;
|
|
714
1024
|
const dispatch = async () => {
|
|
@@ -24,41 +24,6 @@ class FileUploadModule extends abstract_1.BaseServerModule {
|
|
|
24
24
|
app.use(fileUpload(this.fileUploadOptions));
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
app.use(createBunFileUploadMiddleware());
|
|
28
27
|
}
|
|
29
28
|
}
|
|
30
29
|
exports.FileUploadModule = FileUploadModule;
|
|
31
|
-
const createBunFileUploadMiddleware = () => {
|
|
32
|
-
return async (req, _res, next) => {
|
|
33
|
-
if (!req.raw || req.files) {
|
|
34
|
-
return next();
|
|
35
|
-
}
|
|
36
|
-
const contentType = String(req.headers?.["content-type"] || "");
|
|
37
|
-
if (!contentType.includes("multipart/form-data")) {
|
|
38
|
-
return next();
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const formData = await req.raw.formData();
|
|
42
|
-
const files = {};
|
|
43
|
-
const body = {};
|
|
44
|
-
formData.forEach((value, key) => {
|
|
45
|
-
if (value instanceof File) {
|
|
46
|
-
files[key] = value;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
body[key] = value;
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
if (Object.keys(files).length) {
|
|
53
|
-
req.files = files;
|
|
54
|
-
}
|
|
55
|
-
if (Object.keys(body).length && req.body === undefined) {
|
|
56
|
-
req.body = body;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
// Ignore malformed multipart payloads.
|
|
61
|
-
}
|
|
62
|
-
next();
|
|
63
|
-
};
|
|
64
|
-
};
|