bun-platform-kit 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -0
- package/dist/BunKitServer.d.ts +31 -0
- package/dist/BunKitServer.js +173 -0
- package/dist/BunKitStandardServer.d.ts +18 -0
- package/dist/BunKitStandardServer.js +53 -0
- package/dist/abstract/ServerModule.d.ts +7 -0
- package/dist/abstract/ServerModule.js +12 -0
- package/dist/abstract/ServerService.d.ts +6 -0
- package/dist/abstract/ServerService.js +9 -0
- package/dist/abstract/ServerTypes.d.ts +78 -0
- package/dist/abstract/ServerTypes.js +7 -0
- package/dist/abstract/index.d.ts +3 -0
- package/dist/abstract/index.js +19 -0
- package/dist/adapters/BunAdapter.d.ts +12 -0
- package/dist/adapters/BunAdapter.js +1165 -0
- package/dist/adapters/index.d.ts +1 -0
- package/dist/adapters/index.js +17 -0
- package/dist/controllers/ReportFinanceController.d.ts +0 -0
- package/dist/controllers/ReportFinanceController.js +1 -0
- package/dist/decorators/Controller.d.ts +2 -0
- package/dist/decorators/Controller.js +13 -0
- package/dist/decorators/DecoratorGuards.d.ts +1 -0
- package/dist/decorators/DecoratorGuards.js +14 -0
- package/dist/decorators/Handlers.d.ts +18 -0
- package/dist/decorators/Handlers.js +35 -0
- package/dist/decorators/MetadataKeys.d.ts +6 -0
- package/dist/decorators/MetadataKeys.js +10 -0
- package/dist/decorators/Parameters.d.ts +24 -0
- package/dist/decorators/Parameters.js +41 -0
- package/dist/decorators/Use.d.ts +3 -0
- package/dist/decorators/Use.js +26 -0
- package/dist/decorators/index.d.ts +5 -0
- package/dist/decorators/index.js +21 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +27 -0
- package/dist/modules/ControllersModule.d.ts +11 -0
- package/dist/modules/ControllersModule.js +138 -0
- package/dist/modules/CorsModule.d.ts +12 -0
- package/dist/modules/CorsModule.js +89 -0
- package/dist/modules/FileUploadModule.d.ts +21 -0
- package/dist/modules/FileUploadModule.js +20 -0
- package/dist/modules/RateLimitModule.d.ts +18 -0
- package/dist/modules/RateLimitModule.js +61 -0
- package/dist/modules/RequestContextModule.d.ts +18 -0
- package/dist/modules/RequestContextModule.js +52 -0
- package/dist/modules/SecurityModule.d.ts +11 -0
- package/dist/modules/SecurityModule.js +65 -0
- package/dist/modules/index.d.ts +6 -0
- package/dist/modules/index.js +22 -0
- package/dist/testing/createDecoratedTestApp.d.ts +19 -0
- package/dist/testing/createDecoratedTestApp.js +49 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +17 -0
- package/package.json +57 -0
|
@@ -0,0 +1,1165 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.BunAdapter = void 0;
|
|
7
|
+
exports.getFiles = getFiles;
|
|
8
|
+
exports.getFile = getFile;
|
|
9
|
+
const fs_1 = require("fs");
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const ServerTypes_1 = require("../abstract/ServerTypes");
|
|
12
|
+
class BunResponse {
|
|
13
|
+
constructor(cookieJar, handlerTimeoutMs, cookieDefaults, downloadRoot) {
|
|
14
|
+
this.statusCode = 200;
|
|
15
|
+
this.statusExplicitlySet = false;
|
|
16
|
+
this.headers = new Headers();
|
|
17
|
+
this.setCookies = [];
|
|
18
|
+
this.body = null;
|
|
19
|
+
this.ended = false;
|
|
20
|
+
this.cookieJar = cookieJar;
|
|
21
|
+
this.handlerTimeoutMs = handlerTimeoutMs;
|
|
22
|
+
this.cookieDefaults = cookieDefaults;
|
|
23
|
+
this.downloadRoot = downloadRoot ?? DEFAULT_DOWNLOAD_ROOT;
|
|
24
|
+
this.endPromise = new Promise((resolve) => {
|
|
25
|
+
this.resolveEnd = resolve;
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
status(code) {
|
|
29
|
+
if (this.ended) {
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
this.statusExplicitlySet = true;
|
|
33
|
+
this.statusCode = code;
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
set(name, value) {
|
|
37
|
+
if (this.ended) {
|
|
38
|
+
return this;
|
|
39
|
+
}
|
|
40
|
+
if (hasInvalidHeaderValue(value)) {
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
if (name.toLowerCase() === "set-cookie") {
|
|
44
|
+
this.setCookies.push(value);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
this.headers.set(name, value);
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
header(name, value) {
|
|
51
|
+
return this.set(name, value);
|
|
52
|
+
}
|
|
53
|
+
setHeader(name, value) {
|
|
54
|
+
return this.set(name, value);
|
|
55
|
+
}
|
|
56
|
+
cookie(name, value, options = {}) {
|
|
57
|
+
if (this.ended) {
|
|
58
|
+
return this;
|
|
59
|
+
}
|
|
60
|
+
if (!isValidCookieName(name)) {
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
const resolvedOptions = applyCookieDefaults(options, this.cookieDefaults);
|
|
64
|
+
if (this.cookieJar && typeof this.cookieJar.set === "function") {
|
|
65
|
+
this.cookieJar.set(name, value, toCookieJarOptions(resolvedOptions));
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
this.setCookies.push(serializeCookie(name, value, resolvedOptions));
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
json(body) {
|
|
72
|
+
if (this.ended) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
if (!this.headers.has("content-type")) {
|
|
76
|
+
this.headers.set("content-type", "application/json");
|
|
77
|
+
}
|
|
78
|
+
this.body = JSON.stringify(body ?? null);
|
|
79
|
+
this.ended = true;
|
|
80
|
+
this.resolveEnd?.();
|
|
81
|
+
}
|
|
82
|
+
send(body) {
|
|
83
|
+
if (this.ended) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (body instanceof Response) {
|
|
87
|
+
this.rawResponse = body;
|
|
88
|
+
this.ended = true;
|
|
89
|
+
this.resolveEnd?.();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (typeof body === "string" || body instanceof Uint8Array) {
|
|
93
|
+
this.body = body;
|
|
94
|
+
this.ended = true;
|
|
95
|
+
this.resolveEnd?.();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (body === undefined) {
|
|
99
|
+
this.body = null;
|
|
100
|
+
this.ended = true;
|
|
101
|
+
this.resolveEnd?.();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (!this.headers.has("content-type")) {
|
|
105
|
+
this.headers.set("content-type", "application/json");
|
|
106
|
+
}
|
|
107
|
+
this.body = JSON.stringify(body);
|
|
108
|
+
this.ended = true;
|
|
109
|
+
this.resolveEnd?.();
|
|
110
|
+
}
|
|
111
|
+
end(body) {
|
|
112
|
+
if (this.ended) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (body !== undefined) {
|
|
116
|
+
this.send(body);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.ended = true;
|
|
120
|
+
this.resolveEnd?.();
|
|
121
|
+
}
|
|
122
|
+
download(filePath, filename, callback) {
|
|
123
|
+
if (this.ended) {
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const safePath = resolveSafeDownloadPath(this.downloadRoot, filePath);
|
|
128
|
+
const stat = (0, fs_1.statSync)(safePath);
|
|
129
|
+
if (!stat.isFile()) {
|
|
130
|
+
const error = new Error("File not found");
|
|
131
|
+
callback?.(error);
|
|
132
|
+
if (!this.ended) {
|
|
133
|
+
this.status(404).json({ message: "File not found" });
|
|
134
|
+
}
|
|
135
|
+
return this;
|
|
136
|
+
}
|
|
137
|
+
const resolvedName = filename && filename.trim().length > 0
|
|
138
|
+
? filename
|
|
139
|
+
: path_1.default.basename(safePath);
|
|
140
|
+
this.headers.set("content-disposition", `attachment; filename="${sanitizeFilename(resolvedName)}"`);
|
|
141
|
+
const fileBuffer = (0, fs_1.readFileSync)(safePath);
|
|
142
|
+
this.rawResponse = new Response(fileBuffer);
|
|
143
|
+
this.ended = true;
|
|
144
|
+
this.resolveEnd?.();
|
|
145
|
+
callback?.();
|
|
146
|
+
return this;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
callback?.(error);
|
|
150
|
+
if (!this.ended) {
|
|
151
|
+
this.status(500).json({ message: "File download failed" });
|
|
152
|
+
}
|
|
153
|
+
return this;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
isEnded() {
|
|
157
|
+
return this.ended;
|
|
158
|
+
}
|
|
159
|
+
waitForEnd() {
|
|
160
|
+
return this.endPromise;
|
|
161
|
+
}
|
|
162
|
+
getHandlerTimeoutMs() {
|
|
163
|
+
return this.handlerTimeoutMs;
|
|
164
|
+
}
|
|
165
|
+
toResponse() {
|
|
166
|
+
if (this.rawResponse) {
|
|
167
|
+
const headerMap = new Map();
|
|
168
|
+
const setCookies = readSetCookieHeaders(this.rawResponse.headers);
|
|
169
|
+
this.rawResponse.headers.forEach((value, key) => {
|
|
170
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
headerMap.set(key.toLowerCase(), value);
|
|
174
|
+
});
|
|
175
|
+
this.headers.forEach((value, key) => {
|
|
176
|
+
headerMap.set(key.toLowerCase(), value);
|
|
177
|
+
});
|
|
178
|
+
setCookies.push(...this.setCookies);
|
|
179
|
+
const headersInit = [];
|
|
180
|
+
headerMap.forEach((value, key) => {
|
|
181
|
+
headersInit.push([key, value]);
|
|
182
|
+
});
|
|
183
|
+
for (const cookie of setCookies) {
|
|
184
|
+
headersInit.push(["set-cookie", cookie]);
|
|
185
|
+
}
|
|
186
|
+
return new Response(this.rawResponse.body, {
|
|
187
|
+
status: this.statusExplicitlySet
|
|
188
|
+
? this.statusCode
|
|
189
|
+
: this.rawResponse.status,
|
|
190
|
+
headers: headersInit,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
const headersInit = [];
|
|
194
|
+
this.headers.forEach((value, key) => {
|
|
195
|
+
headersInit.push([key, value]);
|
|
196
|
+
});
|
|
197
|
+
for (const cookie of this.setCookies) {
|
|
198
|
+
headersInit.push(["set-cookie", cookie]);
|
|
199
|
+
}
|
|
200
|
+
return new Response(this.body, {
|
|
201
|
+
status: this.statusCode,
|
|
202
|
+
headers: headersInit,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
class BunRouter {
|
|
207
|
+
constructor() {
|
|
208
|
+
this.middlewares = [];
|
|
209
|
+
this.routes = [];
|
|
210
|
+
}
|
|
211
|
+
use(pathOrHandler, ...handlers) {
|
|
212
|
+
if (typeof pathOrHandler === "string") {
|
|
213
|
+
const path = pathOrHandler;
|
|
214
|
+
const resolvedHandlers = normalizeHandlers(handlers).map((handler) => handler instanceof BunRouter
|
|
215
|
+
? this.wrapRouter(handler, path)
|
|
216
|
+
: handler);
|
|
217
|
+
this.middlewares.push({ path, handlers: resolvedHandlers });
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
const resolvedHandlers = normalizeHandlers([pathOrHandler, ...handlers]);
|
|
221
|
+
const wrappedHandlers = resolvedHandlers.map((handler) => handler instanceof BunRouter
|
|
222
|
+
? this.wrapRouter(handler)
|
|
223
|
+
: handler);
|
|
224
|
+
this.middlewares.push({ handlers: wrappedHandlers });
|
|
225
|
+
}
|
|
226
|
+
get(path, ...handlers) {
|
|
227
|
+
this.routes.push({ method: "GET", path, handlers });
|
|
228
|
+
}
|
|
229
|
+
post(path, ...handlers) {
|
|
230
|
+
this.routes.push({ method: "POST", path, handlers });
|
|
231
|
+
}
|
|
232
|
+
put(path, ...handlers) {
|
|
233
|
+
this.routes.push({ method: "PUT", path, handlers });
|
|
234
|
+
}
|
|
235
|
+
delete(path, ...handlers) {
|
|
236
|
+
this.routes.push({ method: "DELETE", path, handlers });
|
|
237
|
+
}
|
|
238
|
+
patch(path, ...handlers) {
|
|
239
|
+
this.routes.push({ method: "PATCH", path, handlers });
|
|
240
|
+
}
|
|
241
|
+
async handle(req, res, done, pathOverride, suppressNotFound) {
|
|
242
|
+
const path = normalizePath(pathOverride ?? req.path);
|
|
243
|
+
const middlewares = this.collectMiddlewares(path);
|
|
244
|
+
const routeMatch = this.matchRoute(req.method, path);
|
|
245
|
+
if (routeMatch) {
|
|
246
|
+
req.params = routeMatch.params;
|
|
247
|
+
}
|
|
248
|
+
const handlers = [
|
|
249
|
+
...middlewares.flatMap((mw) => mw.handlers),
|
|
250
|
+
...(routeMatch ? routeMatch.route.handlers : []),
|
|
251
|
+
];
|
|
252
|
+
let chainCompleted = false;
|
|
253
|
+
let timeoutId;
|
|
254
|
+
let timedOut = false;
|
|
255
|
+
const timeoutMs = res.getHandlerTimeoutMs();
|
|
256
|
+
const timeoutPromise = typeof timeoutMs === "number" && timeoutMs > 0
|
|
257
|
+
? new Promise((resolve) => {
|
|
258
|
+
timeoutId = setTimeout(() => {
|
|
259
|
+
timedOut = true;
|
|
260
|
+
if (!res.isEnded()) {
|
|
261
|
+
res.status(504).json({ message: "Handler timeout" });
|
|
262
|
+
}
|
|
263
|
+
resolve(false);
|
|
264
|
+
}, timeoutMs);
|
|
265
|
+
})
|
|
266
|
+
: null;
|
|
267
|
+
const runPromise = runHandlers(handlers, req, res);
|
|
268
|
+
try {
|
|
269
|
+
chainCompleted = timeoutPromise
|
|
270
|
+
? await Promise.race([runPromise, timeoutPromise])
|
|
271
|
+
: await runPromise;
|
|
272
|
+
}
|
|
273
|
+
catch (error) {
|
|
274
|
+
if (!res.isEnded()) {
|
|
275
|
+
res.status(500).json({ message: "Internal server error" });
|
|
276
|
+
}
|
|
277
|
+
done(error);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
if (timeoutId) {
|
|
282
|
+
clearTimeout(timeoutId);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (timedOut) {
|
|
286
|
+
runPromise.catch(() => { });
|
|
287
|
+
done();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
if (!routeMatch) {
|
|
291
|
+
if (chainCompleted && !res.isEnded() && !suppressNotFound) {
|
|
292
|
+
res.status(404).json({ message: "Not found" });
|
|
293
|
+
}
|
|
294
|
+
if (chainCompleted) {
|
|
295
|
+
done();
|
|
296
|
+
}
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
if (chainCompleted) {
|
|
300
|
+
done();
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
collectMiddlewares(path) {
|
|
304
|
+
return this.middlewares.filter((layer) => {
|
|
305
|
+
if (!layer.path) {
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
const normalized = normalizePath(layer.path);
|
|
309
|
+
if (normalized === "/") {
|
|
310
|
+
return true;
|
|
311
|
+
}
|
|
312
|
+
return path === normalized || path.startsWith(`${normalized}/`);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
matchRoute(method, path) {
|
|
316
|
+
for (const route of this.routes) {
|
|
317
|
+
if (route.method !== method.toUpperCase()) {
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const match = matchPath(route.path, path);
|
|
321
|
+
if (match) {
|
|
322
|
+
return { route, params: match };
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
wrapRouter(router, basePath) {
|
|
328
|
+
return async (req, res, next) => {
|
|
329
|
+
if (!basePath) {
|
|
330
|
+
await router.handle(req, res, next, undefined, true);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
const normalizedBase = normalizePath(basePath);
|
|
334
|
+
const currentPath = normalizePath(req.path);
|
|
335
|
+
if (normalizedBase === "/") {
|
|
336
|
+
await router.handle(req, res, next, currentPath, true);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (currentPath !== normalizedBase &&
|
|
340
|
+
!currentPath.startsWith(`${normalizedBase}/`)) {
|
|
341
|
+
return next();
|
|
342
|
+
}
|
|
343
|
+
const nestedPath = stripPrefix(currentPath, normalizedBase);
|
|
344
|
+
await router.handle(req, res, next, nestedPath, true);
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
class BunApp extends BunRouter {
|
|
349
|
+
constructor() {
|
|
350
|
+
super(...arguments);
|
|
351
|
+
this.settings = new Map();
|
|
352
|
+
this.activeRequests = 0;
|
|
353
|
+
}
|
|
354
|
+
set(key, value) {
|
|
355
|
+
this.settings.set(key, value);
|
|
356
|
+
}
|
|
357
|
+
get(key) {
|
|
358
|
+
return this.settings.get(key);
|
|
359
|
+
}
|
|
360
|
+
createFetchHandler() {
|
|
361
|
+
return async (request, server) => {
|
|
362
|
+
const client = server?.requestIP?.(request);
|
|
363
|
+
const cookieJar = request.cookies;
|
|
364
|
+
const handlerTimeoutSetting = this.get("handlerTimeoutMs");
|
|
365
|
+
const handlerTimeoutMs = handlerTimeoutSetting === undefined
|
|
366
|
+
? DEFAULT_HANDLER_TIMEOUT_MS
|
|
367
|
+
: typeof handlerTimeoutSetting === "number"
|
|
368
|
+
? handlerTimeoutSetting
|
|
369
|
+
: undefined;
|
|
370
|
+
const trustProxy = resolveTrustProxySetting(this);
|
|
371
|
+
const maxConcurrentRequests = Number(this.get("maxConcurrentRequests") ?? 0);
|
|
372
|
+
if (maxConcurrentRequests > 0 && this.activeRequests >= maxConcurrentRequests) {
|
|
373
|
+
return new Response(JSON.stringify({ message: "Server busy" }), {
|
|
374
|
+
status: 503,
|
|
375
|
+
headers: [["content-type", "application/json"]],
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
this.activeRequests += 1;
|
|
379
|
+
const req = createRequest(request, client?.address, trustProxy);
|
|
380
|
+
const res = new BunResponse(cookieJar, handlerTimeoutMs, resolveCookieDefaults(this.get("cookieDefaults")), resolveDownloadRoot(this.get("downloadRoot")));
|
|
381
|
+
try {
|
|
382
|
+
await this.handle(req, res, () => undefined);
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
this.activeRequests = Math.max(0, this.activeRequests - 1);
|
|
386
|
+
}
|
|
387
|
+
if (!res.isEnded()) {
|
|
388
|
+
res.status(204).end();
|
|
389
|
+
}
|
|
390
|
+
return res.toResponse();
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
class BunAdapter {
|
|
395
|
+
constructor() {
|
|
396
|
+
this.runtime = ServerTypes_1.ServerRuntime.Bun;
|
|
397
|
+
}
|
|
398
|
+
createApp() {
|
|
399
|
+
return new BunApp();
|
|
400
|
+
}
|
|
401
|
+
createRouter() {
|
|
402
|
+
return new BunRouter();
|
|
403
|
+
}
|
|
404
|
+
configure(app, _port) {
|
|
405
|
+
const bunApp = app;
|
|
406
|
+
if (shouldEnableSecurityHeaders(bunApp.get("securityHeaders"))) {
|
|
407
|
+
bunApp.use(createSecurityHeadersMiddleware(bunApp));
|
|
408
|
+
}
|
|
409
|
+
bunApp.use(createMultipartBodyParser(bunApp));
|
|
410
|
+
bunApp.use(createJsonBodyParser(bunApp));
|
|
411
|
+
bunApp.use(createUrlEncodedBodyParser(bunApp));
|
|
412
|
+
}
|
|
413
|
+
listen(app, port, onListen) {
|
|
414
|
+
const bunApp = app;
|
|
415
|
+
const server = Bun.serve({
|
|
416
|
+
port,
|
|
417
|
+
fetch: bunApp.createFetchHandler(),
|
|
418
|
+
});
|
|
419
|
+
onListen();
|
|
420
|
+
return {
|
|
421
|
+
close: (callback) => {
|
|
422
|
+
server.stop(true);
|
|
423
|
+
callback?.();
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
exports.BunAdapter = BunAdapter;
|
|
429
|
+
const createJsonBodyParser = (app) => {
|
|
430
|
+
return async (req, res, next) => {
|
|
431
|
+
if (!req.raw || req.body !== undefined || req.files !== undefined) {
|
|
432
|
+
return next();
|
|
433
|
+
}
|
|
434
|
+
const method = String(req.method || "").toUpperCase();
|
|
435
|
+
if (method === "GET" || method === "HEAD") {
|
|
436
|
+
return next();
|
|
437
|
+
}
|
|
438
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
439
|
+
if (!contentType.includes("application/json")) {
|
|
440
|
+
return next();
|
|
441
|
+
}
|
|
442
|
+
const limit = getBodyLimit(app);
|
|
443
|
+
const contentLength = parseContentLength(req.headers["content-length"]);
|
|
444
|
+
if (contentLength === 0) {
|
|
445
|
+
return next();
|
|
446
|
+
}
|
|
447
|
+
if (contentLength !== undefined && contentLength > limit) {
|
|
448
|
+
res.status(413).json({ message: "Payload too large" });
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
if (contentLength === undefined) {
|
|
453
|
+
const text = await readBodyTextWithLimit(req.raw, limit);
|
|
454
|
+
req.body = JSON.parse(text);
|
|
455
|
+
}
|
|
456
|
+
else {
|
|
457
|
+
req.body = await req.raw.json();
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
catch (error) {
|
|
461
|
+
if (error?.status === 413) {
|
|
462
|
+
res.status(413).json({ message: "Payload too large" });
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
res.status(400).json({ message: "Invalid JSON" });
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
next();
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
const createUrlEncodedBodyParser = (app) => {
|
|
472
|
+
return async (req, res, next) => {
|
|
473
|
+
if (!req.raw || req.body !== undefined) {
|
|
474
|
+
return next();
|
|
475
|
+
}
|
|
476
|
+
const method = String(req.method || "").toUpperCase();
|
|
477
|
+
if (method === "GET" || method === "HEAD") {
|
|
478
|
+
return next();
|
|
479
|
+
}
|
|
480
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
481
|
+
if (!contentType.includes("application/x-www-form-urlencoded")) {
|
|
482
|
+
return next();
|
|
483
|
+
}
|
|
484
|
+
const limit = getBodyLimit(app);
|
|
485
|
+
const contentLength = parseContentLength(req.headers["content-length"]);
|
|
486
|
+
if (contentLength !== undefined && contentLength > limit) {
|
|
487
|
+
res.status(413).json({ message: "Payload too large" });
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const text = contentLength === undefined
|
|
492
|
+
? await readBodyTextWithLimit(req.raw, limit)
|
|
493
|
+
: await req.raw.text();
|
|
494
|
+
req.body = Object.fromEntries(new URLSearchParams(text));
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
if (error?.status === 413) {
|
|
498
|
+
res.status(413).json({ message: "Payload too large" });
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
res.status(400).json({ message: "Invalid form data" });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
next();
|
|
505
|
+
};
|
|
506
|
+
};
|
|
507
|
+
const createMultipartBodyParser = (app) => {
|
|
508
|
+
return async (req, res, next) => {
|
|
509
|
+
if (!req.raw || req.body !== undefined || req.files !== undefined) {
|
|
510
|
+
return next();
|
|
511
|
+
}
|
|
512
|
+
const contentType = String(req.headers["content-type"] || "");
|
|
513
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
514
|
+
return next();
|
|
515
|
+
}
|
|
516
|
+
if (app.get("fileUploadEnabled") !== true) {
|
|
517
|
+
res
|
|
518
|
+
.status(415)
|
|
519
|
+
.json({ message: "File uploads are disabled. Enable FileUploadModule." });
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const options = normalizeMultipartOptions(app.get("multipart"));
|
|
523
|
+
const lengthHeader = req.headers["content-length"];
|
|
524
|
+
const contentLength = parseContentLength(lengthHeader);
|
|
525
|
+
// If content-length is missing, body size limits can't be enforced pre-read.
|
|
526
|
+
if (contentLength === undefined) {
|
|
527
|
+
res.status(411).json({ message: "Length required" });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
if (contentLength !== undefined && contentLength > options.maxBodyBytes) {
|
|
531
|
+
res.status(413).json({ message: "Payload too large" });
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
try {
|
|
535
|
+
const formData = await req.raw.formData();
|
|
536
|
+
const fields = {};
|
|
537
|
+
const files = {};
|
|
538
|
+
let fileCount = 0;
|
|
539
|
+
let fieldCount = 0;
|
|
540
|
+
let fieldBytes = 0;
|
|
541
|
+
for (const [key, value] of formData.entries()) {
|
|
542
|
+
if (isFile(value)) {
|
|
543
|
+
if (value.size > options.maxFileBytes) {
|
|
544
|
+
res.status(413).json({ message: "Payload too large" });
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
if (!isMimeAllowed(value.type, options.allowedMimeTypes)) {
|
|
548
|
+
res.status(415).json({ message: "Unsupported media type" });
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
if (options.validateFile) {
|
|
552
|
+
const isValid = await options.validateFile(value);
|
|
553
|
+
if (!isValid) {
|
|
554
|
+
res.status(415).json({ message: "Unsupported media type" });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (options.allowedFileSignatures) {
|
|
559
|
+
const signatureAllowed = await isAllowedFileSignature(value, options.allowedFileSignatures);
|
|
560
|
+
if (!signatureAllowed) {
|
|
561
|
+
res.status(415).json({ message: "Unsupported media type" });
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
fileCount += 1;
|
|
566
|
+
if (fileCount > options.maxFiles) {
|
|
567
|
+
res.status(413).json({ message: "Payload too large" });
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const existing = files[key];
|
|
571
|
+
if (!existing) {
|
|
572
|
+
files[key] = value;
|
|
573
|
+
}
|
|
574
|
+
else if (Array.isArray(existing)) {
|
|
575
|
+
existing.push(value);
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
files[key] = [existing, value];
|
|
579
|
+
}
|
|
580
|
+
continue;
|
|
581
|
+
}
|
|
582
|
+
const existing = fields[key];
|
|
583
|
+
const textValue = String(value);
|
|
584
|
+
fieldCount += 1;
|
|
585
|
+
if (fieldCount > options.maxFields) {
|
|
586
|
+
res.status(413).json({ message: "Payload too large" });
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const textBytes = Buffer.byteLength(textValue, "utf8");
|
|
590
|
+
fieldBytes += textBytes;
|
|
591
|
+
if (textBytes > options.maxFieldBytes || fieldBytes > options.maxFieldsBytes) {
|
|
592
|
+
res.status(413).json({ message: "Payload too large" });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
if (existing === undefined) {
|
|
596
|
+
fields[key] = textValue;
|
|
597
|
+
}
|
|
598
|
+
else if (Array.isArray(existing)) {
|
|
599
|
+
existing.push(textValue);
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
fields[key] = [existing, textValue];
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
if (Object.keys(fields).length > 0) {
|
|
606
|
+
req.body = fields;
|
|
607
|
+
}
|
|
608
|
+
if (Object.keys(files).length > 0) {
|
|
609
|
+
req.files = files;
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
res.status(400).json({ message: "Invalid multipart form data" });
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
next();
|
|
617
|
+
};
|
|
618
|
+
};
|
|
619
|
+
function createRequest(request, remoteAddress, trustProxy) {
|
|
620
|
+
const url = new URL(request.url);
|
|
621
|
+
const headers = toHeaderRecord(request.headers);
|
|
622
|
+
const query = toQueryRecord(url.searchParams);
|
|
623
|
+
const ip = resolveClientIp(headers, remoteAddress, trustProxy);
|
|
624
|
+
return {
|
|
625
|
+
method: request.method.toUpperCase(),
|
|
626
|
+
path: normalizePath(url.pathname),
|
|
627
|
+
originalUrl: url.pathname + url.search,
|
|
628
|
+
params: {},
|
|
629
|
+
query,
|
|
630
|
+
headers,
|
|
631
|
+
cookies: parseCookies(headers.cookie),
|
|
632
|
+
ip,
|
|
633
|
+
raw: request,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
function toHeaderRecord(headers) {
|
|
637
|
+
const record = {};
|
|
638
|
+
headers.forEach((value, key) => {
|
|
639
|
+
record[key.toLowerCase()] = value;
|
|
640
|
+
});
|
|
641
|
+
return record;
|
|
642
|
+
}
|
|
643
|
+
function toQueryRecord(search) {
|
|
644
|
+
const record = {};
|
|
645
|
+
for (const [key, value] of search.entries()) {
|
|
646
|
+
const existing = record[key];
|
|
647
|
+
if (existing === undefined) {
|
|
648
|
+
record[key] = value;
|
|
649
|
+
}
|
|
650
|
+
else if (Array.isArray(existing)) {
|
|
651
|
+
existing.push(value);
|
|
652
|
+
}
|
|
653
|
+
else {
|
|
654
|
+
record[key] = [existing, value];
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return record;
|
|
658
|
+
}
|
|
659
|
+
const DEFAULT_MULTIPART_OPTIONS = {
|
|
660
|
+
maxBodyBytes: 10 * 1024 * 1024,
|
|
661
|
+
maxFileBytes: 10 * 1024 * 1024,
|
|
662
|
+
maxFiles: 10,
|
|
663
|
+
maxFields: 200,
|
|
664
|
+
maxFieldBytes: 64 * 1024,
|
|
665
|
+
maxFieldsBytes: 512 * 1024,
|
|
666
|
+
};
|
|
667
|
+
function serializeCookie(name, value, options) {
|
|
668
|
+
const parts = [`${name}=${encodeURIComponent(value)}`];
|
|
669
|
+
if (options.maxAge !== undefined) {
|
|
670
|
+
const maxAgeSeconds = Math.floor(options.maxAge / 1000);
|
|
671
|
+
parts.push(`Max-Age=${maxAgeSeconds}`);
|
|
672
|
+
if (!options.expires) {
|
|
673
|
+
parts.push(`Expires=${new Date(Date.now() + options.maxAge).toUTCString()}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
if (options.domain) {
|
|
677
|
+
parts.push(`Domain=${options.domain}`);
|
|
678
|
+
}
|
|
679
|
+
if (options.path) {
|
|
680
|
+
parts.push(`Path=${options.path}`);
|
|
681
|
+
}
|
|
682
|
+
if (options.expires) {
|
|
683
|
+
parts.push(`Expires=${options.expires.toUTCString()}`);
|
|
684
|
+
}
|
|
685
|
+
if (options.httpOnly) {
|
|
686
|
+
parts.push("HttpOnly");
|
|
687
|
+
}
|
|
688
|
+
if (options.secure || options.sameSite === "none") {
|
|
689
|
+
parts.push("Secure");
|
|
690
|
+
}
|
|
691
|
+
if (options.sameSite) {
|
|
692
|
+
const normalized = options.sameSite === "none"
|
|
693
|
+
? "None"
|
|
694
|
+
: options.sameSite === "strict"
|
|
695
|
+
? "Strict"
|
|
696
|
+
: "Lax";
|
|
697
|
+
parts.push(`SameSite=${normalized}`);
|
|
698
|
+
}
|
|
699
|
+
return parts.join("; ");
|
|
700
|
+
}
|
|
701
|
+
function toCookieJarOptions(options) {
|
|
702
|
+
const sameSite = options.sameSite === "none"
|
|
703
|
+
? "None"
|
|
704
|
+
: options.sameSite === "strict"
|
|
705
|
+
? "Strict"
|
|
706
|
+
: options.sameSite === "lax"
|
|
707
|
+
? "Lax"
|
|
708
|
+
: undefined;
|
|
709
|
+
const maxAge = options.maxAge === undefined
|
|
710
|
+
? undefined
|
|
711
|
+
: Math.floor(options.maxAge / 1000);
|
|
712
|
+
return {
|
|
713
|
+
maxAge,
|
|
714
|
+
domain: options.domain,
|
|
715
|
+
path: options.path,
|
|
716
|
+
expires: options.expires,
|
|
717
|
+
httpOnly: options.httpOnly,
|
|
718
|
+
secure: options.secure || options.sameSite === "none",
|
|
719
|
+
sameSite,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function getBodyLimit(app) {
|
|
723
|
+
const configured = app.get("bodyLimit");
|
|
724
|
+
if (typeof configured === "number" && Number.isFinite(configured)) {
|
|
725
|
+
return configured;
|
|
726
|
+
}
|
|
727
|
+
return normalizeMultipartOptions(app.get("multipart")).maxBodyBytes;
|
|
728
|
+
}
|
|
729
|
+
function normalizeMultipartOptions(input) {
|
|
730
|
+
if (!input || typeof input !== "object") {
|
|
731
|
+
return { ...DEFAULT_MULTIPART_OPTIONS };
|
|
732
|
+
}
|
|
733
|
+
const value = input;
|
|
734
|
+
return {
|
|
735
|
+
maxBodyBytes: value.maxBodyBytes ?? DEFAULT_MULTIPART_OPTIONS.maxBodyBytes,
|
|
736
|
+
maxFileBytes: value.maxFileBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFileBytes,
|
|
737
|
+
maxFiles: value.maxFiles ?? DEFAULT_MULTIPART_OPTIONS.maxFiles,
|
|
738
|
+
maxFields: value.maxFields ?? DEFAULT_MULTIPART_OPTIONS.maxFields,
|
|
739
|
+
maxFieldBytes: value.maxFieldBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFieldBytes,
|
|
740
|
+
maxFieldsBytes: value.maxFieldsBytes ?? DEFAULT_MULTIPART_OPTIONS.maxFieldsBytes,
|
|
741
|
+
allowedMimeTypes: value.allowedMimeTypes,
|
|
742
|
+
allowedFileSignatures: value.allowedFileSignatures,
|
|
743
|
+
validateFile: value.validateFile,
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
function isMimeAllowed(type, allowed) {
|
|
747
|
+
if (!allowed || allowed.length === 0) {
|
|
748
|
+
return true;
|
|
749
|
+
}
|
|
750
|
+
const normalized = type.toLowerCase();
|
|
751
|
+
for (const entry of allowed) {
|
|
752
|
+
const rule = entry.toLowerCase();
|
|
753
|
+
if (rule.endsWith("/*")) {
|
|
754
|
+
const prefix = rule.slice(0, -1);
|
|
755
|
+
if (normalized.startsWith(prefix)) {
|
|
756
|
+
return true;
|
|
757
|
+
}
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (normalized === rule) {
|
|
761
|
+
return true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return false;
|
|
765
|
+
}
|
|
766
|
+
function isFile(value) {
|
|
767
|
+
if (!value || typeof value !== "object") {
|
|
768
|
+
return false;
|
|
769
|
+
}
|
|
770
|
+
return (typeof value.arrayBuffer === "function" &&
|
|
771
|
+
typeof value.name === "string" &&
|
|
772
|
+
typeof value.size === "number");
|
|
773
|
+
}
|
|
774
|
+
function parseCookies(cookieHeader) {
|
|
775
|
+
if (!cookieHeader) {
|
|
776
|
+
return undefined;
|
|
777
|
+
}
|
|
778
|
+
const cookies = {};
|
|
779
|
+
cookieHeader.split(";").forEach((entry) => {
|
|
780
|
+
const [name, ...rest] = entry.trim().split("=");
|
|
781
|
+
if (!name) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
try {
|
|
785
|
+
cookies[name] = decodeURIComponent(rest.join("="));
|
|
786
|
+
}
|
|
787
|
+
catch {
|
|
788
|
+
cookies[name] = rest.join("=");
|
|
789
|
+
}
|
|
790
|
+
});
|
|
791
|
+
return cookies;
|
|
792
|
+
}
|
|
793
|
+
function parseContentLength(header) {
|
|
794
|
+
if (header === undefined) {
|
|
795
|
+
return undefined;
|
|
796
|
+
}
|
|
797
|
+
const value = Array.isArray(header) ? header[0] : header;
|
|
798
|
+
if (!value) {
|
|
799
|
+
return undefined;
|
|
800
|
+
}
|
|
801
|
+
const parsed = Number.parseInt(value, 10);
|
|
802
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
803
|
+
}
|
|
804
|
+
const DEFAULT_HANDLER_TIMEOUT_MS = 30000;
|
|
805
|
+
const DEFAULT_DOWNLOAD_ROOT = process.cwd();
|
|
806
|
+
function readSetCookieHeaders(headers) {
|
|
807
|
+
const bunHeaders = headers;
|
|
808
|
+
const setCookieFromApi = bunHeaders.getSetCookie?.() ??
|
|
809
|
+
bunHeaders.getAll?.("set-cookie") ??
|
|
810
|
+
bunHeaders.getAll?.("Set-Cookie");
|
|
811
|
+
if (setCookieFromApi && setCookieFromApi.length > 0) {
|
|
812
|
+
return setCookieFromApi;
|
|
813
|
+
}
|
|
814
|
+
const setCookies = [];
|
|
815
|
+
headers.forEach((value, key) => {
|
|
816
|
+
if (key.toLowerCase() === "set-cookie") {
|
|
817
|
+
setCookies.push(value);
|
|
818
|
+
}
|
|
819
|
+
});
|
|
820
|
+
return setCookies;
|
|
821
|
+
}
|
|
822
|
+
function getFiles(req, field) {
|
|
823
|
+
const map = (req.files ?? {});
|
|
824
|
+
const entry = map[field];
|
|
825
|
+
if (!entry) {
|
|
826
|
+
return [];
|
|
827
|
+
}
|
|
828
|
+
return Array.isArray(entry) ? entry : [entry];
|
|
829
|
+
}
|
|
830
|
+
function getFile(req, field) {
|
|
831
|
+
return getFiles(req, field)[0];
|
|
832
|
+
}
|
|
833
|
+
function extractIp(headers) {
|
|
834
|
+
const forwarded = headers["x-forwarded-for"];
|
|
835
|
+
if (forwarded) {
|
|
836
|
+
return forwarded.split(",")[0]?.trim();
|
|
837
|
+
}
|
|
838
|
+
return headers["x-real-ip"];
|
|
839
|
+
}
|
|
840
|
+
function resolveClientIp(headers, remoteAddress, trustProxy) {
|
|
841
|
+
const normalizedRemote = normalizeIp(remoteAddress);
|
|
842
|
+
if (!trustProxy) {
|
|
843
|
+
return normalizedRemote;
|
|
844
|
+
}
|
|
845
|
+
if (trustProxy === true) {
|
|
846
|
+
return extractIp(headers) || normalizedRemote;
|
|
847
|
+
}
|
|
848
|
+
if (typeof trustProxy === "function") {
|
|
849
|
+
return trustProxy(normalizedRemote)
|
|
850
|
+
? extractIp(headers) || normalizedRemote
|
|
851
|
+
: normalizedRemote;
|
|
852
|
+
}
|
|
853
|
+
const trusted = isTrustedProxy(normalizedRemote, trustProxy);
|
|
854
|
+
return trusted ? extractIp(headers) || normalizedRemote : normalizedRemote;
|
|
855
|
+
}
|
|
856
|
+
function normalizeIp(ip) {
|
|
857
|
+
if (!ip) {
|
|
858
|
+
return undefined;
|
|
859
|
+
}
|
|
860
|
+
if (ip.includes(".") && ip.includes(":")) {
|
|
861
|
+
return ip.split(":")[0];
|
|
862
|
+
}
|
|
863
|
+
return ip;
|
|
864
|
+
}
|
|
865
|
+
function isTrustedProxy(ip, allowlist) {
|
|
866
|
+
if (!ip) {
|
|
867
|
+
return false;
|
|
868
|
+
}
|
|
869
|
+
for (const entry of allowlist) {
|
|
870
|
+
if (entry.includes("/")) {
|
|
871
|
+
if (matchesCidr(ip, entry)) {
|
|
872
|
+
return true;
|
|
873
|
+
}
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (entry === ip) {
|
|
877
|
+
return true;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
function matchesCidr(ip, cidr) {
|
|
883
|
+
const [range, bitsString] = cidr.split("/");
|
|
884
|
+
if (!range || !bitsString) {
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
if (range.includes(":") || ip.includes(":")) {
|
|
888
|
+
return range === ip;
|
|
889
|
+
}
|
|
890
|
+
const bits = Number(bitsString);
|
|
891
|
+
if (!Number.isFinite(bits) || bits < 0 || bits > 32) {
|
|
892
|
+
return false;
|
|
893
|
+
}
|
|
894
|
+
const ipValue = ipv4ToInt(ip);
|
|
895
|
+
const rangeValue = ipv4ToInt(range);
|
|
896
|
+
if (ipValue === null || rangeValue === null) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
const mask = bits === 0 ? 0 : ~((1 << (32 - bits)) - 1);
|
|
900
|
+
return (ipValue & mask) === (rangeValue & mask);
|
|
901
|
+
}
|
|
902
|
+
function ipv4ToInt(ip) {
|
|
903
|
+
const parts = ip.split(".").map((part) => Number(part));
|
|
904
|
+
if (parts.length !== 4 || parts.some((part) => part < 0 || part > 255)) {
|
|
905
|
+
return null;
|
|
906
|
+
}
|
|
907
|
+
return ((parts[0] << 24) +
|
|
908
|
+
(parts[1] << 16) +
|
|
909
|
+
(parts[2] << 8) +
|
|
910
|
+
parts[3]) >>> 0;
|
|
911
|
+
}
|
|
912
|
+
function normalizePath(path) {
|
|
913
|
+
if (!path.startsWith("/")) {
|
|
914
|
+
path = `/${path}`;
|
|
915
|
+
}
|
|
916
|
+
if (path.length > 1 && path.endsWith("/")) {
|
|
917
|
+
return path.slice(0, -1);
|
|
918
|
+
}
|
|
919
|
+
return path;
|
|
920
|
+
}
|
|
921
|
+
function stripPrefix(path, prefix) {
|
|
922
|
+
if (!path.startsWith(prefix)) {
|
|
923
|
+
return path;
|
|
924
|
+
}
|
|
925
|
+
const stripped = path.slice(prefix.length);
|
|
926
|
+
return normalizePath(stripped || "/");
|
|
927
|
+
}
|
|
928
|
+
function matchPath(routePath, requestPath) {
|
|
929
|
+
const normalizedRoute = normalizePath(routePath);
|
|
930
|
+
const normalizedRequest = normalizePath(requestPath);
|
|
931
|
+
if (normalizedRoute === normalizedRequest) {
|
|
932
|
+
return {};
|
|
933
|
+
}
|
|
934
|
+
const routeParts = normalizedRoute.split("/").filter(Boolean);
|
|
935
|
+
const requestParts = normalizedRequest.split("/").filter(Boolean);
|
|
936
|
+
if (routeParts.length !== requestParts.length) {
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
const params = {};
|
|
940
|
+
for (let index = 0; index < routeParts.length; index += 1) {
|
|
941
|
+
const routePart = routeParts[index];
|
|
942
|
+
const requestPart = requestParts[index];
|
|
943
|
+
if (routePart.startsWith(":")) {
|
|
944
|
+
try {
|
|
945
|
+
params[routePart.slice(1)] = decodeURIComponent(requestPart);
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (routePart !== requestPart) {
|
|
953
|
+
return null;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
return params;
|
|
957
|
+
}
|
|
958
|
+
function normalizeHandlers(inputs) {
|
|
959
|
+
const handlers = [];
|
|
960
|
+
for (const input of inputs) {
|
|
961
|
+
if (Array.isArray(input)) {
|
|
962
|
+
handlers.push(...input);
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
965
|
+
handlers.push(input);
|
|
966
|
+
}
|
|
967
|
+
return handlers;
|
|
968
|
+
}
|
|
969
|
+
function applyCookieDefaults(options, defaults) {
|
|
970
|
+
if (!defaults) {
|
|
971
|
+
return options;
|
|
972
|
+
}
|
|
973
|
+
const applyTo = defaults.applyTo ?? "session";
|
|
974
|
+
const isSession = options.maxAge === undefined && options.expires === undefined;
|
|
975
|
+
if (applyTo !== "all" && !isSession) {
|
|
976
|
+
return options;
|
|
977
|
+
}
|
|
978
|
+
return { ...defaults.options, ...options };
|
|
979
|
+
}
|
|
980
|
+
function resolveCookieDefaults(input) {
|
|
981
|
+
const baseDefaults = getDefaultCookieDefaults();
|
|
982
|
+
if (!input || typeof input !== "object") {
|
|
983
|
+
return baseDefaults;
|
|
984
|
+
}
|
|
985
|
+
const defaults = input;
|
|
986
|
+
if (!defaults.options || typeof defaults.options !== "object") {
|
|
987
|
+
return baseDefaults;
|
|
988
|
+
}
|
|
989
|
+
return {
|
|
990
|
+
applyTo: defaults.applyTo ?? baseDefaults.applyTo,
|
|
991
|
+
options: { ...baseDefaults.options, ...defaults.options },
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function resolveTrustProxySetting(app) {
|
|
995
|
+
const input = app.get("trustProxy");
|
|
996
|
+
if (typeof input === "boolean") {
|
|
997
|
+
throw new Error("Invalid trustProxy boolean. Use a CIDR allowlist or a custom trust function instead.");
|
|
998
|
+
}
|
|
999
|
+
if (Array.isArray(input) && input.every((entry) => typeof entry === "string")) {
|
|
1000
|
+
return input;
|
|
1001
|
+
}
|
|
1002
|
+
if (typeof input === "function") {
|
|
1003
|
+
return input;
|
|
1004
|
+
}
|
|
1005
|
+
return undefined;
|
|
1006
|
+
}
|
|
1007
|
+
function hasInvalidHeaderValue(value) {
|
|
1008
|
+
return value.includes("\r") || value.includes("\n");
|
|
1009
|
+
}
|
|
1010
|
+
function shouldEnableSecurityHeaders(setting) {
|
|
1011
|
+
if (typeof setting === "boolean") {
|
|
1012
|
+
return setting;
|
|
1013
|
+
}
|
|
1014
|
+
return isProduction();
|
|
1015
|
+
}
|
|
1016
|
+
function getDefaultCookieDefaults() {
|
|
1017
|
+
return {
|
|
1018
|
+
applyTo: "session",
|
|
1019
|
+
options: {
|
|
1020
|
+
httpOnly: true,
|
|
1021
|
+
sameSite: "lax",
|
|
1022
|
+
secure: isProduction(),
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
function isProduction() {
|
|
1027
|
+
return process?.env?.NODE_ENV === "production";
|
|
1028
|
+
}
|
|
1029
|
+
function createSecurityHeadersMiddleware(_app) {
|
|
1030
|
+
return (_req, res, next) => {
|
|
1031
|
+
res.set("x-content-type-options", "nosniff");
|
|
1032
|
+
res.set("referrer-policy", "no-referrer");
|
|
1033
|
+
res.set("x-frame-options", "DENY");
|
|
1034
|
+
next();
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
async function readBodyTextWithLimit(request, limit) {
|
|
1038
|
+
const bytes = await readBodyBytesWithLimit(request, limit);
|
|
1039
|
+
return new TextDecoder().decode(bytes);
|
|
1040
|
+
}
|
|
1041
|
+
async function readBodyBytesWithLimit(request, limit) {
|
|
1042
|
+
if (!request.body) {
|
|
1043
|
+
return new Uint8Array();
|
|
1044
|
+
}
|
|
1045
|
+
const reader = request.body.getReader();
|
|
1046
|
+
const chunks = [];
|
|
1047
|
+
let total = 0;
|
|
1048
|
+
while (true) {
|
|
1049
|
+
const { done, value } = await reader.read();
|
|
1050
|
+
if (done) {
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
if (value) {
|
|
1054
|
+
total += value.length;
|
|
1055
|
+
if (total > limit) {
|
|
1056
|
+
const error = new Error("Payload too large");
|
|
1057
|
+
error.status = 413;
|
|
1058
|
+
throw error;
|
|
1059
|
+
}
|
|
1060
|
+
chunks.push(value);
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const result = new Uint8Array(total);
|
|
1064
|
+
let offset = 0;
|
|
1065
|
+
for (const chunk of chunks) {
|
|
1066
|
+
result.set(chunk, offset);
|
|
1067
|
+
offset += chunk.length;
|
|
1068
|
+
}
|
|
1069
|
+
return result;
|
|
1070
|
+
}
|
|
1071
|
+
async function isAllowedFileSignature(file, allowed) {
|
|
1072
|
+
const head = typeof file.slice === "function" ? file.slice(0, 32) : file;
|
|
1073
|
+
const buffer = new Uint8Array(await head.arrayBuffer());
|
|
1074
|
+
for (const kind of allowed) {
|
|
1075
|
+
if (matchesSignature(buffer, kind)) {
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
return false;
|
|
1080
|
+
}
|
|
1081
|
+
function matchesSignature(buffer, kind) {
|
|
1082
|
+
if (kind === "png") {
|
|
1083
|
+
return (buffer.length >= 8 &&
|
|
1084
|
+
buffer[0] === 0x89 &&
|
|
1085
|
+
buffer[1] === 0x50 &&
|
|
1086
|
+
buffer[2] === 0x4e &&
|
|
1087
|
+
buffer[3] === 0x47 &&
|
|
1088
|
+
buffer[4] === 0x0d &&
|
|
1089
|
+
buffer[5] === 0x0a &&
|
|
1090
|
+
buffer[6] === 0x1a &&
|
|
1091
|
+
buffer[7] === 0x0a);
|
|
1092
|
+
}
|
|
1093
|
+
if (kind === "pdf") {
|
|
1094
|
+
return (buffer.length >= 4 &&
|
|
1095
|
+
buffer[0] === 0x25 &&
|
|
1096
|
+
buffer[1] === 0x50 &&
|
|
1097
|
+
buffer[2] === 0x44 &&
|
|
1098
|
+
buffer[3] === 0x46);
|
|
1099
|
+
}
|
|
1100
|
+
// jpg/jpeg
|
|
1101
|
+
return (buffer.length >= 3 &&
|
|
1102
|
+
buffer[0] === 0xff &&
|
|
1103
|
+
buffer[1] === 0xd8 &&
|
|
1104
|
+
buffer[2] === 0xff);
|
|
1105
|
+
}
|
|
1106
|
+
function isValidCookieName(name) {
|
|
1107
|
+
return /^[!#$%&'*+\-.^_|~0-9A-Za-z]+$/.test(name);
|
|
1108
|
+
}
|
|
1109
|
+
function sanitizeFilename(value) {
|
|
1110
|
+
return value.replace(/[/\\"]/g, "_");
|
|
1111
|
+
}
|
|
1112
|
+
function resolveDownloadRoot(input) {
|
|
1113
|
+
if (typeof input === "string" && input.trim().length > 0) {
|
|
1114
|
+
return path_1.default.resolve(input);
|
|
1115
|
+
}
|
|
1116
|
+
return DEFAULT_DOWNLOAD_ROOT;
|
|
1117
|
+
}
|
|
1118
|
+
function resolveSafeDownloadPath(root, inputPath) {
|
|
1119
|
+
const resolvedRoot = path_1.default.resolve(root);
|
|
1120
|
+
const resolved = path_1.default.resolve(resolvedRoot, inputPath);
|
|
1121
|
+
const rootPrefix = resolvedRoot === path_1.default.parse(resolvedRoot).root
|
|
1122
|
+
? resolvedRoot
|
|
1123
|
+
: `${resolvedRoot}${path_1.default.sep}`;
|
|
1124
|
+
if (!resolved.startsWith(rootPrefix)) {
|
|
1125
|
+
throw new Error("Invalid path");
|
|
1126
|
+
}
|
|
1127
|
+
return resolved;
|
|
1128
|
+
}
|
|
1129
|
+
async function runHandlers(handlers, req, res) {
|
|
1130
|
+
let index = 0;
|
|
1131
|
+
const dispatch = async () => {
|
|
1132
|
+
const handler = handlers[index];
|
|
1133
|
+
index += 1;
|
|
1134
|
+
if (!handler) {
|
|
1135
|
+
return true;
|
|
1136
|
+
}
|
|
1137
|
+
let nextCalled = false;
|
|
1138
|
+
let nextPromise;
|
|
1139
|
+
let resolveNext;
|
|
1140
|
+
const nextSignal = new Promise((resolve) => {
|
|
1141
|
+
resolveNext = resolve;
|
|
1142
|
+
});
|
|
1143
|
+
const wrappedNext = (nextErr) => {
|
|
1144
|
+
if (nextErr) {
|
|
1145
|
+
throw nextErr;
|
|
1146
|
+
}
|
|
1147
|
+
nextCalled = true;
|
|
1148
|
+
resolveNext?.();
|
|
1149
|
+
nextPromise = dispatch();
|
|
1150
|
+
return nextPromise;
|
|
1151
|
+
};
|
|
1152
|
+
await handler(req, res, wrappedNext);
|
|
1153
|
+
if (!nextCalled) {
|
|
1154
|
+
if (res.isEnded()) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
await Promise.race([nextSignal, res.waitForEnd()]);
|
|
1158
|
+
if (!nextCalled || res.isEnded()) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return nextPromise ? await nextPromise : true;
|
|
1163
|
+
};
|
|
1164
|
+
return dispatch();
|
|
1165
|
+
}
|