@morojs/moro 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/LICENSE +21 -0
- package/README.md +233 -0
- package/dist/core/config/index.d.ts +19 -0
- package/dist/core/config/index.js +59 -0
- package/dist/core/config/index.js.map +1 -0
- package/dist/core/config/loader.d.ts +6 -0
- package/dist/core/config/loader.js +288 -0
- package/dist/core/config/loader.js.map +1 -0
- package/dist/core/config/schema.d.ts +335 -0
- package/dist/core/config/schema.js +286 -0
- package/dist/core/config/schema.js.map +1 -0
- package/dist/core/config/utils.d.ts +50 -0
- package/dist/core/config/utils.js +185 -0
- package/dist/core/config/utils.js.map +1 -0
- package/dist/core/database/adapters/drizzle.d.ts +29 -0
- package/dist/core/database/adapters/drizzle.js +366 -0
- package/dist/core/database/adapters/drizzle.js.map +1 -0
- package/dist/core/database/adapters/index.d.ts +8 -0
- package/dist/core/database/adapters/index.js +48 -0
- package/dist/core/database/adapters/index.js.map +1 -0
- package/dist/core/database/adapters/mongodb.d.ts +35 -0
- package/dist/core/database/adapters/mongodb.js +215 -0
- package/dist/core/database/adapters/mongodb.js.map +1 -0
- package/dist/core/database/adapters/mysql.d.ts +23 -0
- package/dist/core/database/adapters/mysql.js +149 -0
- package/dist/core/database/adapters/mysql.js.map +1 -0
- package/dist/core/database/adapters/postgresql.d.ts +24 -0
- package/dist/core/database/adapters/postgresql.js +160 -0
- package/dist/core/database/adapters/postgresql.js.map +1 -0
- package/dist/core/database/adapters/redis.d.ts +50 -0
- package/dist/core/database/adapters/redis.js +266 -0
- package/dist/core/database/adapters/redis.js.map +1 -0
- package/dist/core/database/adapters/sqlite.d.ts +23 -0
- package/dist/core/database/adapters/sqlite.js +194 -0
- package/dist/core/database/adapters/sqlite.js.map +1 -0
- package/dist/core/database/index.d.ts +2 -0
- package/dist/core/database/index.js +20 -0
- package/dist/core/database/index.js.map +1 -0
- package/dist/core/docs/index.d.ts +63 -0
- package/dist/core/docs/index.js +170 -0
- package/dist/core/docs/index.js.map +1 -0
- package/dist/core/docs/openapi-generator.d.ts +124 -0
- package/dist/core/docs/openapi-generator.js +413 -0
- package/dist/core/docs/openapi-generator.js.map +1 -0
- package/dist/core/docs/simple-docs.d.ts +21 -0
- package/dist/core/docs/simple-docs.js +268 -0
- package/dist/core/docs/simple-docs.js.map +1 -0
- package/dist/core/docs/swagger-ui.d.ts +28 -0
- package/dist/core/docs/swagger-ui.js +317 -0
- package/dist/core/docs/swagger-ui.js.map +1 -0
- package/dist/core/docs/zod-to-openapi.d.ts +29 -0
- package/dist/core/docs/zod-to-openapi.js +414 -0
- package/dist/core/docs/zod-to-openapi.js.map +1 -0
- package/dist/core/events/event-bus.d.ts +27 -0
- package/dist/core/events/event-bus.js +193 -0
- package/dist/core/events/event-bus.js.map +1 -0
- package/dist/core/events/index.d.ts +2 -0
- package/dist/core/events/index.js +7 -0
- package/dist/core/events/index.js.map +1 -0
- package/dist/core/framework.d.ts +57 -0
- package/dist/core/framework.js +432 -0
- package/dist/core/framework.js.map +1 -0
- package/dist/core/http/http-server.d.ts +114 -0
- package/dist/core/http/http-server.js +1154 -0
- package/dist/core/http/http-server.js.map +1 -0
- package/dist/core/http/index.d.ts +3 -0
- package/dist/core/http/index.js +10 -0
- package/dist/core/http/index.js.map +1 -0
- package/dist/core/http/router.d.ts +14 -0
- package/dist/core/http/router.js +113 -0
- package/dist/core/http/router.js.map +1 -0
- package/dist/core/logger/filters.d.ts +9 -0
- package/dist/core/logger/filters.js +134 -0
- package/dist/core/logger/filters.js.map +1 -0
- package/dist/core/logger/index.d.ts +3 -0
- package/dist/core/logger/index.js +26 -0
- package/dist/core/logger/index.js.map +1 -0
- package/dist/core/logger/logger.d.ts +49 -0
- package/dist/core/logger/logger.js +332 -0
- package/dist/core/logger/logger.js.map +1 -0
- package/dist/core/logger/outputs.d.ts +42 -0
- package/dist/core/logger/outputs.js +110 -0
- package/dist/core/logger/outputs.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cache/file.d.ts +15 -0
- package/dist/core/middleware/built-in/adapters/cache/file.js +128 -0
- package/dist/core/middleware/built-in/adapters/cache/file.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cache/index.d.ts +5 -0
- package/dist/core/middleware/built-in/adapters/cache/index.js +28 -0
- package/dist/core/middleware/built-in/adapters/cache/index.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cache/memory.d.ts +11 -0
- package/dist/core/middleware/built-in/adapters/cache/memory.js +65 -0
- package/dist/core/middleware/built-in/adapters/cache/memory.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cache/redis.d.ts +17 -0
- package/dist/core/middleware/built-in/adapters/cache/redis.js +91 -0
- package/dist/core/middleware/built-in/adapters/cache/redis.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cdn/azure.d.ts +21 -0
- package/dist/core/middleware/built-in/adapters/cdn/azure.js +40 -0
- package/dist/core/middleware/built-in/adapters/cdn/azure.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cdn/cloudflare.d.ts +14 -0
- package/dist/core/middleware/built-in/adapters/cdn/cloudflare.js +77 -0
- package/dist/core/middleware/built-in/adapters/cdn/cloudflare.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cdn/cloudfront.d.ts +15 -0
- package/dist/core/middleware/built-in/adapters/cdn/cloudfront.js +73 -0
- package/dist/core/middleware/built-in/adapters/cdn/cloudfront.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/cdn/index.d.ts +5 -0
- package/dist/core/middleware/built-in/adapters/cdn/index.js +28 -0
- package/dist/core/middleware/built-in/adapters/cdn/index.js.map +1 -0
- package/dist/core/middleware/built-in/adapters/index.d.ts +4 -0
- package/dist/core/middleware/built-in/adapters/index.js +26 -0
- package/dist/core/middleware/built-in/adapters/index.js.map +1 -0
- package/dist/core/middleware/built-in/auth.d.ts +2 -0
- package/dist/core/middleware/built-in/auth.js +38 -0
- package/dist/core/middleware/built-in/auth.js.map +1 -0
- package/dist/core/middleware/built-in/cache.d.ts +3 -0
- package/dist/core/middleware/built-in/cache.js +188 -0
- package/dist/core/middleware/built-in/cache.js.map +1 -0
- package/dist/core/middleware/built-in/cdn.d.ts +3 -0
- package/dist/core/middleware/built-in/cdn.js +115 -0
- package/dist/core/middleware/built-in/cdn.js.map +1 -0
- package/dist/core/middleware/built-in/cookie.d.ts +14 -0
- package/dist/core/middleware/built-in/cookie.js +68 -0
- package/dist/core/middleware/built-in/cookie.js.map +1 -0
- package/dist/core/middleware/built-in/cors.d.ts +2 -0
- package/dist/core/middleware/built-in/cors.js +29 -0
- package/dist/core/middleware/built-in/cors.js.map +1 -0
- package/dist/core/middleware/built-in/csp.d.ts +22 -0
- package/dist/core/middleware/built-in/csp.js +74 -0
- package/dist/core/middleware/built-in/csp.js.map +1 -0
- package/dist/core/middleware/built-in/csrf.d.ts +9 -0
- package/dist/core/middleware/built-in/csrf.js +66 -0
- package/dist/core/middleware/built-in/csrf.js.map +1 -0
- package/dist/core/middleware/built-in/error-tracker.d.ts +1 -0
- package/dist/core/middleware/built-in/error-tracker.js +19 -0
- package/dist/core/middleware/built-in/error-tracker.js.map +1 -0
- package/dist/core/middleware/built-in/index.d.ts +70 -0
- package/dist/core/middleware/built-in/index.js +70 -0
- package/dist/core/middleware/built-in/index.js.map +1 -0
- package/dist/core/middleware/built-in/performance-monitor.d.ts +1 -0
- package/dist/core/middleware/built-in/performance-monitor.js +22 -0
- package/dist/core/middleware/built-in/performance-monitor.js.map +1 -0
- package/dist/core/middleware/built-in/rate-limit.d.ts +6 -0
- package/dist/core/middleware/built-in/rate-limit.js +47 -0
- package/dist/core/middleware/built-in/rate-limit.js.map +1 -0
- package/dist/core/middleware/built-in/request-logger.d.ts +1 -0
- package/dist/core/middleware/built-in/request-logger.js +15 -0
- package/dist/core/middleware/built-in/request-logger.js.map +1 -0
- package/dist/core/middleware/built-in/session.d.ts +41 -0
- package/dist/core/middleware/built-in/session.js +209 -0
- package/dist/core/middleware/built-in/session.js.map +1 -0
- package/dist/core/middleware/built-in/sse.d.ts +6 -0
- package/dist/core/middleware/built-in/sse.js +73 -0
- package/dist/core/middleware/built-in/sse.js.map +1 -0
- package/dist/core/middleware/built-in/validation.d.ts +2 -0
- package/dist/core/middleware/built-in/validation.js +31 -0
- package/dist/core/middleware/built-in/validation.js.map +1 -0
- package/dist/core/middleware/index.d.ts +21 -0
- package/dist/core/middleware/index.js +152 -0
- package/dist/core/middleware/index.js.map +1 -0
- package/dist/core/modules/auto-discovery.d.ts +27 -0
- package/dist/core/modules/auto-discovery.js +255 -0
- package/dist/core/modules/auto-discovery.js.map +1 -0
- package/dist/core/modules/index.d.ts +2 -0
- package/dist/core/modules/index.js +11 -0
- package/dist/core/modules/index.js.map +1 -0
- package/dist/core/modules/modules.d.ts +10 -0
- package/dist/core/modules/modules.js +137 -0
- package/dist/core/modules/modules.js.map +1 -0
- package/dist/core/networking/index.d.ts +2 -0
- package/dist/core/networking/index.js +9 -0
- package/dist/core/networking/index.js.map +1 -0
- package/dist/core/networking/service-discovery.d.ts +38 -0
- package/dist/core/networking/service-discovery.js +233 -0
- package/dist/core/networking/service-discovery.js.map +1 -0
- package/dist/core/networking/websocket-manager.d.ts +27 -0
- package/dist/core/networking/websocket-manager.js +211 -0
- package/dist/core/networking/websocket-manager.js.map +1 -0
- package/dist/core/routing/app-integration.d.ts +42 -0
- package/dist/core/routing/app-integration.js +152 -0
- package/dist/core/routing/app-integration.js.map +1 -0
- package/dist/core/routing/index.d.ts +106 -0
- package/dist/core/routing/index.js +343 -0
- package/dist/core/routing/index.js.map +1 -0
- package/dist/core/runtime/aws-lambda-adapter.d.ts +43 -0
- package/dist/core/runtime/aws-lambda-adapter.js +108 -0
- package/dist/core/runtime/aws-lambda-adapter.js.map +1 -0
- package/dist/core/runtime/base-adapter.d.ts +16 -0
- package/dist/core/runtime/base-adapter.js +105 -0
- package/dist/core/runtime/base-adapter.js.map +1 -0
- package/dist/core/runtime/cloudflare-workers-adapter.d.ts +18 -0
- package/dist/core/runtime/cloudflare-workers-adapter.js +131 -0
- package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -0
- package/dist/core/runtime/index.d.ts +14 -0
- package/dist/core/runtime/index.js +56 -0
- package/dist/core/runtime/index.js.map +1 -0
- package/dist/core/runtime/node-adapter.d.ts +15 -0
- package/dist/core/runtime/node-adapter.js +204 -0
- package/dist/core/runtime/node-adapter.js.map +1 -0
- package/dist/core/runtime/vercel-edge-adapter.d.ts +10 -0
- package/dist/core/runtime/vercel-edge-adapter.js +106 -0
- package/dist/core/runtime/vercel-edge-adapter.js.map +1 -0
- package/dist/core/utilities/circuit-breaker.d.ts +14 -0
- package/dist/core/utilities/circuit-breaker.js +42 -0
- package/dist/core/utilities/circuit-breaker.js.map +1 -0
- package/dist/core/utilities/container.d.ts +116 -0
- package/dist/core/utilities/container.js +529 -0
- package/dist/core/utilities/container.js.map +1 -0
- package/dist/core/utilities/hooks.d.ts +24 -0
- package/dist/core/utilities/hooks.js +131 -0
- package/dist/core/utilities/hooks.js.map +1 -0
- package/dist/core/utilities/index.d.ts +4 -0
- package/dist/core/utilities/index.js +22 -0
- package/dist/core/utilities/index.js.map +1 -0
- package/dist/core/validation/index.d.ts +30 -0
- package/dist/core/validation/index.js +144 -0
- package/dist/core/validation/index.js.map +1 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/moro.d.ts +82 -0
- package/dist/moro.js +679 -0
- package/dist/moro.js.map +1 -0
- package/dist/types/cache.d.ts +34 -0
- package/dist/types/cache.js +3 -0
- package/dist/types/cache.js.map +1 -0
- package/dist/types/cdn.d.ts +19 -0
- package/dist/types/cdn.js +3 -0
- package/dist/types/cdn.js.map +1 -0
- package/dist/types/core.d.ts +13 -0
- package/dist/types/core.js +3 -0
- package/dist/types/core.js.map +1 -0
- package/dist/types/database.d.ts +29 -0
- package/dist/types/database.js +3 -0
- package/dist/types/database.js.map +1 -0
- package/dist/types/discovery.d.ts +6 -0
- package/dist/types/discovery.js +3 -0
- package/dist/types/discovery.js.map +1 -0
- package/dist/types/events.d.ts +116 -0
- package/dist/types/events.js +3 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/hooks.d.ts +38 -0
- package/dist/types/hooks.js +3 -0
- package/dist/types/hooks.js.map +1 -0
- package/dist/types/http.d.ts +51 -0
- package/dist/types/http.js +3 -0
- package/dist/types/http.js.map +1 -0
- package/dist/types/logger.d.ts +77 -0
- package/dist/types/logger.js +3 -0
- package/dist/types/logger.js.map +1 -0
- package/dist/types/module.d.ts +91 -0
- package/dist/types/module.js +3 -0
- package/dist/types/module.js.map +1 -0
- package/dist/types/runtime.d.ts +48 -0
- package/dist/types/runtime.js +3 -0
- package/dist/types/runtime.js.map +1 -0
- package/dist/types/session.d.ts +66 -0
- package/dist/types/session.js +3 -0
- package/dist/types/session.js.map +1 -0
- package/package.json +176 -0
- package/src/core/config/index.ts +47 -0
- package/src/core/config/loader.ts +366 -0
- package/src/core/config/schema.ts +346 -0
- package/src/core/config/utils.ts +220 -0
- package/src/core/database/README.md +228 -0
- package/src/core/database/adapters/drizzle.ts +425 -0
- package/src/core/database/adapters/index.ts +45 -0
- package/src/core/database/adapters/mongodb.ts +292 -0
- package/src/core/database/adapters/mysql.ts +217 -0
- package/src/core/database/adapters/postgresql.ts +211 -0
- package/src/core/database/adapters/redis.ts +331 -0
- package/src/core/database/adapters/sqlite.ts +255 -0
- package/src/core/database/index.ts +3 -0
- package/src/core/docs/index.ts +245 -0
- package/src/core/docs/openapi-generator.ts +588 -0
- package/src/core/docs/simple-docs.ts +305 -0
- package/src/core/docs/swagger-ui.ts +370 -0
- package/src/core/docs/zod-to-openapi.ts +532 -0
- package/src/core/events/event-bus.ts +249 -0
- package/src/core/events/index.ts +12 -0
- package/src/core/framework.ts +621 -0
- package/src/core/http/http-server.ts +1421 -0
- package/src/core/http/index.ts +11 -0
- package/src/core/http/router.ts +153 -0
- package/src/core/logger/filters.ts +148 -0
- package/src/core/logger/index.ts +20 -0
- package/src/core/logger/logger.ts +434 -0
- package/src/core/logger/outputs.ts +136 -0
- package/src/core/middleware/built-in/adapters/cache/file.ts +106 -0
- package/src/core/middleware/built-in/adapters/cache/index.ts +26 -0
- package/src/core/middleware/built-in/adapters/cache/memory.ts +73 -0
- package/src/core/middleware/built-in/adapters/cache/redis.ts +103 -0
- package/src/core/middleware/built-in/adapters/cdn/azure.ts +68 -0
- package/src/core/middleware/built-in/adapters/cdn/cloudflare.ts +100 -0
- package/src/core/middleware/built-in/adapters/cdn/cloudfront.ts +92 -0
- package/src/core/middleware/built-in/adapters/cdn/index.ts +23 -0
- package/src/core/middleware/built-in/adapters/index.ts +7 -0
- package/src/core/middleware/built-in/auth.ts +39 -0
- package/src/core/middleware/built-in/cache.ts +228 -0
- package/src/core/middleware/built-in/cdn.ts +151 -0
- package/src/core/middleware/built-in/cookie.ts +90 -0
- package/src/core/middleware/built-in/cors.ts +38 -0
- package/src/core/middleware/built-in/csp.ts +107 -0
- package/src/core/middleware/built-in/csrf.ts +87 -0
- package/src/core/middleware/built-in/error-tracker.ts +16 -0
- package/src/core/middleware/built-in/index.ts +57 -0
- package/src/core/middleware/built-in/performance-monitor.ts +25 -0
- package/src/core/middleware/built-in/rate-limit.ts +60 -0
- package/src/core/middleware/built-in/request-logger.ts +14 -0
- package/src/core/middleware/built-in/session.ts +311 -0
- package/src/core/middleware/built-in/sse.ts +91 -0
- package/src/core/middleware/built-in/validation.ts +33 -0
- package/src/core/middleware/index.ts +188 -0
- package/src/core/modules/auto-discovery.ts +265 -0
- package/src/core/modules/index.ts +6 -0
- package/src/core/modules/modules.ts +125 -0
- package/src/core/networking/index.ts +7 -0
- package/src/core/networking/service-discovery.ts +309 -0
- package/src/core/networking/websocket-manager.ts +259 -0
- package/src/core/routing/app-integration.ts +229 -0
- package/src/core/routing/index.ts +519 -0
- package/src/core/runtime/aws-lambda-adapter.ts +157 -0
- package/src/core/runtime/base-adapter.ts +140 -0
- package/src/core/runtime/cloudflare-workers-adapter.ts +166 -0
- package/src/core/runtime/index.ts +74 -0
- package/src/core/runtime/node-adapter.ts +210 -0
- package/src/core/runtime/vercel-edge-adapter.ts +125 -0
- package/src/core/utilities/circuit-breaker.ts +46 -0
- package/src/core/utilities/container.ts +760 -0
- package/src/core/utilities/hooks.ts +148 -0
- package/src/core/utilities/index.ts +16 -0
- package/src/core/validation/index.ts +216 -0
- package/src/index.ts +120 -0
- package/src/moro.ts +842 -0
- package/src/types/cache.ts +38 -0
- package/src/types/cdn.ts +22 -0
- package/src/types/core.ts +17 -0
- package/src/types/database.ts +40 -0
- package/src/types/discovery.ts +7 -0
- package/src/types/events.ts +90 -0
- package/src/types/hooks.ts +47 -0
- package/src/types/http.ts +70 -0
- package/src/types/logger.ts +109 -0
- package/src/types/module.ts +87 -0
- package/src/types/runtime.ts +91 -0
- package/src/types/session.ts +89 -0
- package/tsconfig.json +21 -0
|
@@ -0,0 +1,1421 @@
|
|
|
1
|
+
// src/core/http-server.ts
|
|
2
|
+
import { IncomingMessage, ServerResponse, createServer, Server } from "http";
|
|
3
|
+
import { URL } from "url";
|
|
4
|
+
import * as zlib from "zlib";
|
|
5
|
+
import { promisify } from "util";
|
|
6
|
+
import { createFrameworkLogger } from "../logger";
|
|
7
|
+
import {
|
|
8
|
+
HttpRequest,
|
|
9
|
+
HttpResponse,
|
|
10
|
+
HttpHandler,
|
|
11
|
+
Middleware,
|
|
12
|
+
RouteEntry,
|
|
13
|
+
} from "../../types/http";
|
|
14
|
+
|
|
15
|
+
const gzip = promisify(zlib.gzip);
|
|
16
|
+
const deflate = promisify(zlib.deflate);
|
|
17
|
+
|
|
18
|
+
export class MoroHttpServer {
|
|
19
|
+
private server: Server;
|
|
20
|
+
private routes: RouteEntry[] = [];
|
|
21
|
+
private globalMiddleware: Middleware[] = [];
|
|
22
|
+
private compressionEnabled = true;
|
|
23
|
+
private compressionThreshold = 1024;
|
|
24
|
+
private logger = createFrameworkLogger("HttpServer");
|
|
25
|
+
|
|
26
|
+
constructor() {
|
|
27
|
+
this.server = createServer(this.handleRequest.bind(this));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Middleware management
|
|
31
|
+
use(middleware: Middleware): void {
|
|
32
|
+
this.globalMiddleware.push(middleware);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Routing methods
|
|
36
|
+
get(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
|
|
37
|
+
this.addRoute("GET", path, handlers);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
post(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
|
|
41
|
+
this.addRoute("POST", path, handlers);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
put(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
|
|
45
|
+
this.addRoute("PUT", path, handlers);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
delete(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
|
|
49
|
+
this.addRoute("DELETE", path, handlers);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
patch(path: string, ...handlers: (Middleware | HttpHandler)[]): void {
|
|
53
|
+
this.addRoute("PATCH", path, handlers);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private addRoute(
|
|
57
|
+
method: string,
|
|
58
|
+
path: string,
|
|
59
|
+
handlers: (Middleware | HttpHandler)[],
|
|
60
|
+
): void {
|
|
61
|
+
const { pattern, paramNames } = this.pathToRegex(path);
|
|
62
|
+
const handler = handlers.pop() as HttpHandler;
|
|
63
|
+
const middleware = handlers as Middleware[];
|
|
64
|
+
|
|
65
|
+
this.routes.push({
|
|
66
|
+
method,
|
|
67
|
+
path,
|
|
68
|
+
pattern,
|
|
69
|
+
paramNames,
|
|
70
|
+
handler,
|
|
71
|
+
middleware,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private pathToRegex(path: string): { pattern: RegExp; paramNames: string[] } {
|
|
76
|
+
const paramNames: string[] = [];
|
|
77
|
+
|
|
78
|
+
// Convert parameterized routes to regex
|
|
79
|
+
const regexPattern = path
|
|
80
|
+
.replace(/\/:([^/]+)/g, (match, paramName) => {
|
|
81
|
+
paramNames.push(paramName);
|
|
82
|
+
return "/([^/]+)";
|
|
83
|
+
})
|
|
84
|
+
.replace(/\//g, "\\/");
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
pattern: new RegExp(`^${regexPattern}$`),
|
|
88
|
+
paramNames,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async handleRequest(
|
|
93
|
+
req: IncomingMessage,
|
|
94
|
+
res: ServerResponse,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
const httpReq = this.enhanceRequest(req);
|
|
97
|
+
const httpRes = this.enhanceResponse(res);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Parse URL and query parameters
|
|
101
|
+
const url = new URL(req.url!, `http://${req.headers.host}`);
|
|
102
|
+
httpReq.path = url.pathname;
|
|
103
|
+
httpReq.query = Object.fromEntries(url.searchParams);
|
|
104
|
+
|
|
105
|
+
// Parse body for POST/PUT/PATCH requests
|
|
106
|
+
if (["POST", "PUT", "PATCH"].includes(req.method!)) {
|
|
107
|
+
httpReq.body = await this.parseBody(req);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Execute global middleware first
|
|
111
|
+
await this.executeMiddleware(this.globalMiddleware, httpReq, httpRes);
|
|
112
|
+
|
|
113
|
+
// If middleware handled the request, don't continue
|
|
114
|
+
if (httpRes.headersSent) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Find matching route
|
|
119
|
+
const route = this.findRoute(req.method!, httpReq.path);
|
|
120
|
+
if (!route) {
|
|
121
|
+
httpRes.status(404).json({ success: false, error: "Not found" });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Extract path parameters
|
|
126
|
+
const matches = httpReq.path.match(route.pattern);
|
|
127
|
+
if (matches) {
|
|
128
|
+
httpReq.params = {};
|
|
129
|
+
route.paramNames.forEach((name, index) => {
|
|
130
|
+
httpReq.params[name] = matches[index + 1];
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Execute middleware chain
|
|
135
|
+
await this.executeMiddleware(route.middleware, httpReq, httpRes);
|
|
136
|
+
|
|
137
|
+
// Execute handler
|
|
138
|
+
await route.handler(httpReq, httpRes);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
this.logger.error("Request error", "RequestHandler", {
|
|
141
|
+
error: error instanceof Error ? error.message : String(error),
|
|
142
|
+
requestId: httpReq.requestId,
|
|
143
|
+
method: req.method,
|
|
144
|
+
path: req.url,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
if (!httpRes.headersSent) {
|
|
148
|
+
httpRes.status(500).json({
|
|
149
|
+
success: false,
|
|
150
|
+
error: "Internal server error",
|
|
151
|
+
requestId: httpReq.requestId,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private enhanceRequest(req: IncomingMessage): HttpRequest {
|
|
158
|
+
const httpReq = req as HttpRequest;
|
|
159
|
+
httpReq.params = {};
|
|
160
|
+
httpReq.query = {};
|
|
161
|
+
httpReq.body = null;
|
|
162
|
+
httpReq.path = "";
|
|
163
|
+
httpReq.ip = req.socket.remoteAddress || "";
|
|
164
|
+
httpReq.requestId = Math.random().toString(36).substring(7);
|
|
165
|
+
httpReq.headers = req.headers as Record<string, string>;
|
|
166
|
+
|
|
167
|
+
// Parse cookies
|
|
168
|
+
httpReq.cookies = this.parseCookies(req.headers.cookie || "");
|
|
169
|
+
|
|
170
|
+
return httpReq;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private parseCookies(cookieHeader: string): Record<string, string> {
|
|
174
|
+
const cookies: Record<string, string> = {};
|
|
175
|
+
if (!cookieHeader) return cookies;
|
|
176
|
+
|
|
177
|
+
cookieHeader.split(";").forEach((cookie) => {
|
|
178
|
+
const [name, value] = cookie.trim().split("=");
|
|
179
|
+
if (name && value) {
|
|
180
|
+
cookies[name] = decodeURIComponent(value);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return cookies;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private enhanceResponse(res: ServerResponse): HttpResponse {
|
|
188
|
+
const httpRes = res as HttpResponse;
|
|
189
|
+
|
|
190
|
+
httpRes.json = async (data: any) => {
|
|
191
|
+
if (httpRes.headersSent) return;
|
|
192
|
+
|
|
193
|
+
const jsonString = JSON.stringify(data);
|
|
194
|
+
const buffer = Buffer.from(jsonString);
|
|
195
|
+
|
|
196
|
+
httpRes.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
197
|
+
|
|
198
|
+
// Compression
|
|
199
|
+
if (
|
|
200
|
+
this.compressionEnabled &&
|
|
201
|
+
buffer.length > this.compressionThreshold
|
|
202
|
+
) {
|
|
203
|
+
const acceptEncoding = httpRes.req.headers["accept-encoding"] || "";
|
|
204
|
+
|
|
205
|
+
if (acceptEncoding.includes("gzip")) {
|
|
206
|
+
const compressed = await gzip(buffer);
|
|
207
|
+
httpRes.setHeader("Content-Encoding", "gzip");
|
|
208
|
+
httpRes.setHeader("Content-Length", compressed.length);
|
|
209
|
+
httpRes.end(compressed);
|
|
210
|
+
return;
|
|
211
|
+
} else if (acceptEncoding.includes("deflate")) {
|
|
212
|
+
const compressed = await deflate(buffer);
|
|
213
|
+
httpRes.setHeader("Content-Encoding", "deflate");
|
|
214
|
+
httpRes.setHeader("Content-Length", compressed.length);
|
|
215
|
+
httpRes.end(compressed);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
httpRes.setHeader("Content-Length", buffer.length);
|
|
221
|
+
httpRes.end(buffer);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
httpRes.status = (code: number) => {
|
|
225
|
+
httpRes.statusCode = code;
|
|
226
|
+
return httpRes;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
httpRes.send = (data: string | Buffer) => {
|
|
230
|
+
if (httpRes.headersSent) return;
|
|
231
|
+
|
|
232
|
+
// Auto-detect content type if not already set
|
|
233
|
+
if (!httpRes.getHeader("Content-Type")) {
|
|
234
|
+
if (typeof data === "string") {
|
|
235
|
+
// Check if it's JSON
|
|
236
|
+
try {
|
|
237
|
+
JSON.parse(data);
|
|
238
|
+
httpRes.setHeader(
|
|
239
|
+
"Content-Type",
|
|
240
|
+
"application/json; charset=utf-8",
|
|
241
|
+
);
|
|
242
|
+
} catch {
|
|
243
|
+
// Default to plain text
|
|
244
|
+
httpRes.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
245
|
+
}
|
|
246
|
+
} else {
|
|
247
|
+
// Buffer data - default to octet-stream
|
|
248
|
+
httpRes.setHeader("Content-Type", "application/octet-stream");
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
httpRes.end(data);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
httpRes.cookie = (name: string, value: string, options: any = {}) => {
|
|
256
|
+
const cookieValue = encodeURIComponent(value);
|
|
257
|
+
let cookieString = `${name}=${cookieValue}`;
|
|
258
|
+
|
|
259
|
+
if (options.maxAge) cookieString += `; Max-Age=${options.maxAge}`;
|
|
260
|
+
if (options.expires)
|
|
261
|
+
cookieString += `; Expires=${options.expires.toUTCString()}`;
|
|
262
|
+
if (options.httpOnly) cookieString += "; HttpOnly";
|
|
263
|
+
if (options.secure) cookieString += "; Secure";
|
|
264
|
+
if (options.sameSite) cookieString += `; SameSite=${options.sameSite}`;
|
|
265
|
+
if (options.domain) cookieString += `; Domain=${options.domain}`;
|
|
266
|
+
if (options.path) cookieString += `; Path=${options.path}`;
|
|
267
|
+
|
|
268
|
+
const existingCookies = httpRes.getHeader("Set-Cookie") || [];
|
|
269
|
+
const cookies = Array.isArray(existingCookies)
|
|
270
|
+
? [...existingCookies]
|
|
271
|
+
: [existingCookies as string];
|
|
272
|
+
cookies.push(cookieString);
|
|
273
|
+
httpRes.setHeader("Set-Cookie", cookies);
|
|
274
|
+
|
|
275
|
+
return httpRes;
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
httpRes.clearCookie = (name: string, options: any = {}) => {
|
|
279
|
+
const clearOptions = { ...options, expires: new Date(0), maxAge: 0 };
|
|
280
|
+
return httpRes.cookie(name, "", clearOptions);
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
httpRes.redirect = (url: string, status: number = 302) => {
|
|
284
|
+
if (httpRes.headersSent) return;
|
|
285
|
+
httpRes.statusCode = status;
|
|
286
|
+
httpRes.setHeader("Location", url);
|
|
287
|
+
httpRes.end();
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
httpRes.sendFile = async (filePath: string) => {
|
|
291
|
+
if (httpRes.headersSent) return;
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const fs = await import("fs/promises");
|
|
295
|
+
const path = await import("path");
|
|
296
|
+
const extension = path.extname(filePath);
|
|
297
|
+
const mime = await this.getMimeType(extension);
|
|
298
|
+
|
|
299
|
+
const stats = await fs.stat(filePath);
|
|
300
|
+
const data = await fs.readFile(filePath);
|
|
301
|
+
|
|
302
|
+
// Add charset for text-based files
|
|
303
|
+
const contentType = this.addCharsetIfNeeded(mime);
|
|
304
|
+
httpRes.setHeader("Content-Type", contentType);
|
|
305
|
+
httpRes.setHeader("Content-Length", stats.size);
|
|
306
|
+
|
|
307
|
+
// Add security headers for file downloads
|
|
308
|
+
httpRes.setHeader("X-Content-Type-Options", "nosniff");
|
|
309
|
+
|
|
310
|
+
// Add caching headers
|
|
311
|
+
httpRes.setHeader("Last-Modified", stats.mtime.toUTCString());
|
|
312
|
+
httpRes.setHeader("Cache-Control", "public, max-age=31536000"); // 1 year for static files
|
|
313
|
+
|
|
314
|
+
httpRes.end(data);
|
|
315
|
+
} catch (error) {
|
|
316
|
+
httpRes.status(404).json({ success: false, error: "File not found" });
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return httpRes;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async getMimeType(ext: string): Promise<string> {
|
|
324
|
+
const mimeTypes: Record<string, string> = {
|
|
325
|
+
".html": "text/html",
|
|
326
|
+
".css": "text/css",
|
|
327
|
+
".js": "application/javascript",
|
|
328
|
+
".json": "application/json",
|
|
329
|
+
".png": "image/png",
|
|
330
|
+
".jpg": "image/jpeg",
|
|
331
|
+
".jpeg": "image/jpeg",
|
|
332
|
+
".gif": "image/gif",
|
|
333
|
+
".svg": "image/svg+xml",
|
|
334
|
+
".ico": "image/x-icon",
|
|
335
|
+
".pdf": "application/pdf",
|
|
336
|
+
".txt": "text/plain",
|
|
337
|
+
".xml": "application/xml",
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return mimeTypes[ext.toLowerCase()] || "application/octet-stream";
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
private addCharsetIfNeeded(mimeType: string): string {
|
|
344
|
+
// Add charset for text-based content types
|
|
345
|
+
const textTypes = [
|
|
346
|
+
"text/",
|
|
347
|
+
"application/json",
|
|
348
|
+
"application/javascript",
|
|
349
|
+
"application/xml",
|
|
350
|
+
"image/svg+xml",
|
|
351
|
+
];
|
|
352
|
+
|
|
353
|
+
const needsCharset = textTypes.some((type) => mimeType.startsWith(type));
|
|
354
|
+
|
|
355
|
+
if (needsCharset && !mimeType.includes("charset")) {
|
|
356
|
+
return `${mimeType}; charset=utf-8`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return mimeType;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private async parseBody(req: IncomingMessage): Promise<any> {
|
|
363
|
+
return new Promise((resolve, reject) => {
|
|
364
|
+
const chunks: Buffer[] = [];
|
|
365
|
+
let totalLength = 0;
|
|
366
|
+
const maxSize = 10 * 1024 * 1024; // 10MB limit
|
|
367
|
+
|
|
368
|
+
req.on("data", (chunk: Buffer) => {
|
|
369
|
+
totalLength += chunk.length;
|
|
370
|
+
if (totalLength > maxSize) {
|
|
371
|
+
reject(new Error("Request body too large"));
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
chunks.push(chunk);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
req.on("end", () => {
|
|
378
|
+
try {
|
|
379
|
+
const body = Buffer.concat(chunks);
|
|
380
|
+
const contentType = req.headers["content-type"] || "";
|
|
381
|
+
|
|
382
|
+
if (contentType.includes("application/json")) {
|
|
383
|
+
resolve(JSON.parse(body.toString()));
|
|
384
|
+
} else if (
|
|
385
|
+
contentType.includes("application/x-www-form-urlencoded")
|
|
386
|
+
) {
|
|
387
|
+
resolve(this.parseUrlEncoded(body.toString()));
|
|
388
|
+
} else if (contentType.includes("multipart/form-data")) {
|
|
389
|
+
resolve(this.parseMultipart(body, contentType));
|
|
390
|
+
} else {
|
|
391
|
+
resolve(body.toString());
|
|
392
|
+
}
|
|
393
|
+
} catch (error) {
|
|
394
|
+
reject(error);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
req.on("error", reject);
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
private parseMultipart(
|
|
403
|
+
buffer: Buffer,
|
|
404
|
+
contentType: string,
|
|
405
|
+
): { fields: Record<string, string>; files: Record<string, any> } {
|
|
406
|
+
const boundary = contentType.split("boundary=")[1];
|
|
407
|
+
if (!boundary) {
|
|
408
|
+
throw new Error("Invalid multipart boundary");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const parts = buffer.toString("binary").split("--" + boundary);
|
|
412
|
+
const fields: Record<string, string> = {};
|
|
413
|
+
const files: Record<string, any> = {};
|
|
414
|
+
|
|
415
|
+
for (let i = 1; i < parts.length - 1; i++) {
|
|
416
|
+
const part = parts[i];
|
|
417
|
+
const [headers, content] = part.split("\r\n\r\n");
|
|
418
|
+
|
|
419
|
+
if (!headers || content === undefined) continue;
|
|
420
|
+
|
|
421
|
+
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
422
|
+
const filenameMatch = headers.match(/filename="([^"]+)"/);
|
|
423
|
+
const contentTypeMatch = headers.match(/Content-Type: ([^\r\n]+)/);
|
|
424
|
+
|
|
425
|
+
if (nameMatch) {
|
|
426
|
+
const name = nameMatch[1];
|
|
427
|
+
|
|
428
|
+
if (filenameMatch) {
|
|
429
|
+
// This is a file
|
|
430
|
+
const filename = filenameMatch[1];
|
|
431
|
+
const mimeType = contentTypeMatch
|
|
432
|
+
? contentTypeMatch[1]
|
|
433
|
+
: "application/octet-stream";
|
|
434
|
+
const fileContent = content.substring(0, content.length - 2); // Remove trailing \r\n
|
|
435
|
+
|
|
436
|
+
files[name] = {
|
|
437
|
+
filename,
|
|
438
|
+
mimetype: mimeType,
|
|
439
|
+
data: Buffer.from(fileContent, "binary"),
|
|
440
|
+
size: Buffer.byteLength(fileContent, "binary"),
|
|
441
|
+
};
|
|
442
|
+
} else {
|
|
443
|
+
// This is a regular field
|
|
444
|
+
fields[name] = content.substring(0, content.length - 2); // Remove trailing \r\n
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return { fields, files };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private parseUrlEncoded(body: string): Record<string, string> {
|
|
453
|
+
const params = new URLSearchParams(body);
|
|
454
|
+
const result: Record<string, string> = {};
|
|
455
|
+
for (const [key, value] of params) {
|
|
456
|
+
result[key] = value;
|
|
457
|
+
}
|
|
458
|
+
return result;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private findRoute(method: string, path: string): RouteEntry | null {
|
|
462
|
+
return (
|
|
463
|
+
this.routes.find(
|
|
464
|
+
(route) => route.method === method && route.pattern.test(path),
|
|
465
|
+
) || null
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private async executeMiddleware(
|
|
470
|
+
middleware: Middleware[],
|
|
471
|
+
req: HttpRequest,
|
|
472
|
+
res: HttpResponse,
|
|
473
|
+
): Promise<void> {
|
|
474
|
+
for (const mw of middleware) {
|
|
475
|
+
await new Promise<void>((resolve, reject) => {
|
|
476
|
+
let nextCalled = false;
|
|
477
|
+
|
|
478
|
+
const next = () => {
|
|
479
|
+
if (nextCalled) return;
|
|
480
|
+
nextCalled = true;
|
|
481
|
+
resolve();
|
|
482
|
+
};
|
|
483
|
+
|
|
484
|
+
try {
|
|
485
|
+
const result = mw(req, res, next);
|
|
486
|
+
|
|
487
|
+
// Handle async middleware
|
|
488
|
+
if (result instanceof Promise) {
|
|
489
|
+
result
|
|
490
|
+
.then(() => {
|
|
491
|
+
if (!nextCalled) next();
|
|
492
|
+
})
|
|
493
|
+
.catch(reject);
|
|
494
|
+
}
|
|
495
|
+
} catch (error) {
|
|
496
|
+
reject(error);
|
|
497
|
+
}
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
listen(port: number, callback?: () => void): void;
|
|
503
|
+
listen(port: number, host: string, callback?: () => void): void;
|
|
504
|
+
listen(
|
|
505
|
+
port: number,
|
|
506
|
+
host?: string | (() => void),
|
|
507
|
+
callback?: () => void,
|
|
508
|
+
): void {
|
|
509
|
+
// Handle overloaded parameters (port, callback) or (port, host, callback)
|
|
510
|
+
if (typeof host === "function") {
|
|
511
|
+
callback = host;
|
|
512
|
+
host = undefined;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
if (host) {
|
|
516
|
+
this.server.listen(port, host, callback);
|
|
517
|
+
} else {
|
|
518
|
+
this.server.listen(port, callback);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
close(): Promise<void> {
|
|
523
|
+
return new Promise((resolve) => {
|
|
524
|
+
this.server.close(() => resolve());
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
getServer(): Server {
|
|
529
|
+
return this.server;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// Built-in middleware
|
|
534
|
+
export const middleware = {
|
|
535
|
+
cors: (
|
|
536
|
+
options: { origin?: string; credentials?: boolean } = {},
|
|
537
|
+
): Middleware => {
|
|
538
|
+
return (req, res, next) => {
|
|
539
|
+
res.setHeader("Access-Control-Allow-Origin", options.origin || "*");
|
|
540
|
+
res.setHeader(
|
|
541
|
+
"Access-Control-Allow-Methods",
|
|
542
|
+
"GET, POST, PUT, DELETE, OPTIONS",
|
|
543
|
+
);
|
|
544
|
+
res.setHeader(
|
|
545
|
+
"Access-Control-Allow-Headers",
|
|
546
|
+
"Content-Type, Authorization",
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
if (options.credentials) {
|
|
550
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (req.method === "OPTIONS") {
|
|
554
|
+
res.status(200).send("");
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
next();
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
helmet: (): Middleware => {
|
|
563
|
+
return (req, res, next) => {
|
|
564
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
565
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
566
|
+
res.setHeader("X-XSS-Protection", "1; mode=block");
|
|
567
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
568
|
+
res.setHeader(
|
|
569
|
+
"Strict-Transport-Security",
|
|
570
|
+
"max-age=31536000; includeSubDomains",
|
|
571
|
+
);
|
|
572
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'");
|
|
573
|
+
next();
|
|
574
|
+
};
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
compression: (
|
|
578
|
+
options: { threshold?: number; level?: number } = {},
|
|
579
|
+
): Middleware => {
|
|
580
|
+
const zlib = require("zlib");
|
|
581
|
+
const threshold = options.threshold || 1024;
|
|
582
|
+
const level = options.level || 6;
|
|
583
|
+
|
|
584
|
+
return (req, res, next) => {
|
|
585
|
+
const acceptEncoding = req.headers["accept-encoding"] || "";
|
|
586
|
+
|
|
587
|
+
// Override res.json to compress responses
|
|
588
|
+
const originalJson = res.json;
|
|
589
|
+
const originalSend = res.send;
|
|
590
|
+
|
|
591
|
+
const compressResponse = (data: any, isJson = false) => {
|
|
592
|
+
const content = isJson ? JSON.stringify(data) : data;
|
|
593
|
+
const buffer = Buffer.from(content);
|
|
594
|
+
|
|
595
|
+
if (buffer.length < threshold) {
|
|
596
|
+
return isJson
|
|
597
|
+
? originalJson.call(res, data)
|
|
598
|
+
: originalSend.call(res, data);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (acceptEncoding.includes("gzip")) {
|
|
602
|
+
res.setHeader("Content-Encoding", "gzip");
|
|
603
|
+
zlib.gzip(buffer, { level }, (err: any, compressed: Buffer) => {
|
|
604
|
+
if (err) {
|
|
605
|
+
return isJson
|
|
606
|
+
? originalJson.call(res, data)
|
|
607
|
+
: originalSend.call(res, data);
|
|
608
|
+
}
|
|
609
|
+
res.setHeader("Content-Length", compressed.length);
|
|
610
|
+
res.writeHead(res.statusCode || 200, res.getHeaders());
|
|
611
|
+
res.end(compressed);
|
|
612
|
+
});
|
|
613
|
+
} else if (acceptEncoding.includes("deflate")) {
|
|
614
|
+
res.setHeader("Content-Encoding", "deflate");
|
|
615
|
+
zlib.deflate(buffer, { level }, (err: any, compressed: Buffer) => {
|
|
616
|
+
if (err) {
|
|
617
|
+
return isJson
|
|
618
|
+
? originalJson.call(res, data)
|
|
619
|
+
: originalSend.call(res, data);
|
|
620
|
+
}
|
|
621
|
+
res.setHeader("Content-Length", compressed.length);
|
|
622
|
+
res.writeHead(res.statusCode || 200, res.getHeaders());
|
|
623
|
+
res.end(compressed);
|
|
624
|
+
});
|
|
625
|
+
} else {
|
|
626
|
+
return isJson
|
|
627
|
+
? originalJson.call(res, data)
|
|
628
|
+
: originalSend.call(res, data);
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
res.json = function (data: any) {
|
|
633
|
+
// Ensure charset is set for Safari compatibility
|
|
634
|
+
this.setHeader("Content-Type", "application/json; charset=utf-8");
|
|
635
|
+
compressResponse(data, true);
|
|
636
|
+
return this;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
res.send = function (data: any) {
|
|
640
|
+
compressResponse(data, false);
|
|
641
|
+
return this;
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
next();
|
|
645
|
+
};
|
|
646
|
+
},
|
|
647
|
+
|
|
648
|
+
requestLogger: (): Middleware => {
|
|
649
|
+
return (req, res, next) => {
|
|
650
|
+
const start = Date.now();
|
|
651
|
+
res.on("finish", () => {
|
|
652
|
+
const duration = Date.now() - start;
|
|
653
|
+
// Request completed - logged by framework
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
next();
|
|
657
|
+
};
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
bodySize: (options: { limit?: string } = {}): Middleware => {
|
|
661
|
+
const limit = options.limit || "10mb";
|
|
662
|
+
const limitBytes = parseSize(limit);
|
|
663
|
+
|
|
664
|
+
return (req, res, next) => {
|
|
665
|
+
const contentLength = parseInt(req.headers["content-length"] || "0");
|
|
666
|
+
|
|
667
|
+
if (contentLength > limitBytes) {
|
|
668
|
+
res.status(413).json({
|
|
669
|
+
success: false,
|
|
670
|
+
error: "Request entity too large",
|
|
671
|
+
limit: limit,
|
|
672
|
+
});
|
|
673
|
+
return;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
next();
|
|
677
|
+
};
|
|
678
|
+
},
|
|
679
|
+
|
|
680
|
+
static: (options: {
|
|
681
|
+
root: string;
|
|
682
|
+
maxAge?: number;
|
|
683
|
+
index?: string[];
|
|
684
|
+
dotfiles?: "allow" | "deny" | "ignore";
|
|
685
|
+
etag?: boolean;
|
|
686
|
+
}): Middleware => {
|
|
687
|
+
return async (req, res, next) => {
|
|
688
|
+
// Only handle GET and HEAD requests
|
|
689
|
+
if (req.method !== "GET" && req.method !== "HEAD") {
|
|
690
|
+
next();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
try {
|
|
695
|
+
const fs = await import("fs/promises");
|
|
696
|
+
const path = await import("path");
|
|
697
|
+
const crypto = await import("crypto");
|
|
698
|
+
|
|
699
|
+
let filePath = path.join(options.root, req.path);
|
|
700
|
+
|
|
701
|
+
// Security: prevent directory traversal
|
|
702
|
+
if (!filePath.startsWith(path.resolve(options.root))) {
|
|
703
|
+
res.status(403).json({ success: false, error: "Forbidden" });
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Handle dotfiles
|
|
708
|
+
const basename = path.basename(filePath);
|
|
709
|
+
if (basename.startsWith(".")) {
|
|
710
|
+
if (options.dotfiles === "deny") {
|
|
711
|
+
res.status(403).json({ success: false, error: "Forbidden" });
|
|
712
|
+
return;
|
|
713
|
+
} else if (options.dotfiles === "ignore") {
|
|
714
|
+
next();
|
|
715
|
+
return;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let stats;
|
|
720
|
+
try {
|
|
721
|
+
stats = await fs.stat(filePath);
|
|
722
|
+
} catch (error) {
|
|
723
|
+
next(); // File not found, let other middleware handle
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Handle directories
|
|
728
|
+
if (stats.isDirectory()) {
|
|
729
|
+
const indexFiles = options.index || ["index.html", "index.htm"];
|
|
730
|
+
let indexFound = false;
|
|
731
|
+
|
|
732
|
+
for (const indexFile of indexFiles) {
|
|
733
|
+
const indexPath = path.join(filePath, indexFile);
|
|
734
|
+
try {
|
|
735
|
+
const indexStats = await fs.stat(indexPath);
|
|
736
|
+
if (indexStats.isFile()) {
|
|
737
|
+
filePath = indexPath;
|
|
738
|
+
stats = indexStats;
|
|
739
|
+
indexFound = true;
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
} catch (error) {
|
|
743
|
+
// Continue to next index file
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (!indexFound) {
|
|
748
|
+
next();
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// Set headers with proper mime type and charset
|
|
754
|
+
const ext = path.extname(filePath);
|
|
755
|
+
const mimeTypes: Record<string, string> = {
|
|
756
|
+
".html": "text/html",
|
|
757
|
+
".css": "text/css",
|
|
758
|
+
".js": "application/javascript",
|
|
759
|
+
".json": "application/json",
|
|
760
|
+
".png": "image/png",
|
|
761
|
+
".jpg": "image/jpeg",
|
|
762
|
+
".jpeg": "image/jpeg",
|
|
763
|
+
".gif": "image/gif",
|
|
764
|
+
".svg": "image/svg+xml",
|
|
765
|
+
".ico": "image/x-icon",
|
|
766
|
+
".pdf": "application/pdf",
|
|
767
|
+
".txt": "text/plain",
|
|
768
|
+
".xml": "application/xml",
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const baseMimeType =
|
|
772
|
+
mimeTypes[ext.toLowerCase()] || "application/octet-stream";
|
|
773
|
+
|
|
774
|
+
// Add charset for text-based files
|
|
775
|
+
const textTypes = [
|
|
776
|
+
"text/",
|
|
777
|
+
"application/json",
|
|
778
|
+
"application/javascript",
|
|
779
|
+
"application/xml",
|
|
780
|
+
"image/svg+xml",
|
|
781
|
+
];
|
|
782
|
+
const needsCharset = textTypes.some((type) =>
|
|
783
|
+
baseMimeType.startsWith(type),
|
|
784
|
+
);
|
|
785
|
+
const contentType = needsCharset
|
|
786
|
+
? `${baseMimeType}; charset=utf-8`
|
|
787
|
+
: baseMimeType;
|
|
788
|
+
|
|
789
|
+
res.setHeader("Content-Type", contentType);
|
|
790
|
+
res.setHeader("Content-Length", stats.size);
|
|
791
|
+
|
|
792
|
+
// Cache headers
|
|
793
|
+
if (options.maxAge) {
|
|
794
|
+
res.setHeader("Cache-Control", `public, max-age=${options.maxAge}`);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// ETag support
|
|
798
|
+
if (options.etag !== false) {
|
|
799
|
+
const etag = crypto
|
|
800
|
+
.createHash("md5")
|
|
801
|
+
.update(`${stats.mtime.getTime()}-${stats.size}`)
|
|
802
|
+
.digest("hex");
|
|
803
|
+
res.setHeader("ETag", `"${etag}"`);
|
|
804
|
+
|
|
805
|
+
// Handle conditional requests
|
|
806
|
+
const ifNoneMatch = req.headers["if-none-match"];
|
|
807
|
+
if (ifNoneMatch === `"${etag}"`) {
|
|
808
|
+
res.statusCode = 304;
|
|
809
|
+
res.end();
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// Handle HEAD requests
|
|
815
|
+
if (req.method === "HEAD") {
|
|
816
|
+
res.end();
|
|
817
|
+
return;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// Send file
|
|
821
|
+
const data = await fs.readFile(filePath);
|
|
822
|
+
res.end(data);
|
|
823
|
+
} catch (error) {
|
|
824
|
+
res
|
|
825
|
+
.status(500)
|
|
826
|
+
.json({ success: false, error: "Internal server error" });
|
|
827
|
+
}
|
|
828
|
+
};
|
|
829
|
+
},
|
|
830
|
+
|
|
831
|
+
upload: (
|
|
832
|
+
options: {
|
|
833
|
+
dest?: string;
|
|
834
|
+
maxFileSize?: number;
|
|
835
|
+
maxFiles?: number;
|
|
836
|
+
allowedTypes?: string[];
|
|
837
|
+
} = {},
|
|
838
|
+
): Middleware => {
|
|
839
|
+
return (req, res, next) => {
|
|
840
|
+
const contentType = req.headers["content-type"] || "";
|
|
841
|
+
|
|
842
|
+
if (!contentType.includes("multipart/form-data")) {
|
|
843
|
+
next();
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// File upload handling is now built into parseBody method
|
|
848
|
+
// This middleware can add additional validation
|
|
849
|
+
if (req.body && req.body.files) {
|
|
850
|
+
const files = req.body.files;
|
|
851
|
+
const maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default
|
|
852
|
+
const maxFiles = options.maxFiles || 10;
|
|
853
|
+
const allowedTypes = options.allowedTypes;
|
|
854
|
+
|
|
855
|
+
// Validate file count
|
|
856
|
+
if (Object.keys(files).length > maxFiles) {
|
|
857
|
+
res.status(400).json({
|
|
858
|
+
success: false,
|
|
859
|
+
error: `Too many files. Maximum ${maxFiles} allowed.`,
|
|
860
|
+
});
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Validate each file
|
|
865
|
+
for (const [fieldName, file] of Object.entries(files)) {
|
|
866
|
+
const fileData = file as any;
|
|
867
|
+
|
|
868
|
+
// Validate file size
|
|
869
|
+
if (fileData.size > maxFileSize) {
|
|
870
|
+
res.status(400).json({
|
|
871
|
+
success: false,
|
|
872
|
+
error: `File ${fileData.filename} is too large. Maximum ${maxFileSize} bytes allowed.`,
|
|
873
|
+
});
|
|
874
|
+
return;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Validate file type
|
|
878
|
+
if (allowedTypes && !allowedTypes.includes(fileData.mimetype)) {
|
|
879
|
+
res.status(400).json({
|
|
880
|
+
success: false,
|
|
881
|
+
error: `File type ${fileData.mimetype} not allowed.`,
|
|
882
|
+
});
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Store files in request for easy access
|
|
888
|
+
req.files = files;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
next();
|
|
892
|
+
};
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
template: (options: {
|
|
896
|
+
views: string;
|
|
897
|
+
engine?: "moro" | "handlebars" | "ejs";
|
|
898
|
+
cache?: boolean;
|
|
899
|
+
defaultLayout?: string;
|
|
900
|
+
}): Middleware => {
|
|
901
|
+
const templateCache = new Map<string, string>();
|
|
902
|
+
|
|
903
|
+
return async (req, res, next) => {
|
|
904
|
+
// Add render method to response
|
|
905
|
+
res.render = async (template: string, data: any = {}) => {
|
|
906
|
+
try {
|
|
907
|
+
const fs = await import("fs/promises");
|
|
908
|
+
const path = await import("path");
|
|
909
|
+
|
|
910
|
+
const templatePath = path.join(options.views, `${template}.html`);
|
|
911
|
+
|
|
912
|
+
let templateContent: string;
|
|
913
|
+
|
|
914
|
+
// Check cache first
|
|
915
|
+
if (options.cache && templateCache.has(templatePath)) {
|
|
916
|
+
templateContent = templateCache.get(templatePath)!;
|
|
917
|
+
} else {
|
|
918
|
+
templateContent = await fs.readFile(templatePath, "utf-8");
|
|
919
|
+
if (options.cache) {
|
|
920
|
+
templateCache.set(templatePath, templateContent);
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Simple template engine - replace {{variable}} with values
|
|
925
|
+
let rendered = templateContent;
|
|
926
|
+
|
|
927
|
+
// Handle basic variable substitution
|
|
928
|
+
rendered = rendered.replace(
|
|
929
|
+
/\{\{(\w+)\}\}/g,
|
|
930
|
+
(match: string, key: string) => {
|
|
931
|
+
return data[key] !== undefined ? String(data[key]) : match;
|
|
932
|
+
},
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
// Handle nested object properties like {{user.name}}
|
|
936
|
+
rendered = rendered.replace(
|
|
937
|
+
/\{\{([\w.]+)\}\}/g,
|
|
938
|
+
(match: string, key: string) => {
|
|
939
|
+
const value = key
|
|
940
|
+
.split(".")
|
|
941
|
+
.reduce((obj: any, prop: string) => obj?.[prop], data);
|
|
942
|
+
return value !== undefined ? String(value) : match;
|
|
943
|
+
},
|
|
944
|
+
);
|
|
945
|
+
|
|
946
|
+
// Handle loops: {{#each items}}{{name}}{{/each}}
|
|
947
|
+
rendered = rendered.replace(
|
|
948
|
+
/\{\{#each (\w+)\}\}(.*?)\{\{\/each\}\}/gs,
|
|
949
|
+
(match, arrayKey, template) => {
|
|
950
|
+
const array = data[arrayKey];
|
|
951
|
+
if (!Array.isArray(array)) return "";
|
|
952
|
+
|
|
953
|
+
return array
|
|
954
|
+
.map((item) => {
|
|
955
|
+
let itemTemplate = template;
|
|
956
|
+
// Replace variables in the loop template
|
|
957
|
+
itemTemplate = itemTemplate.replace(
|
|
958
|
+
/\{\{(\w+)\}\}/g,
|
|
959
|
+
(match: string, key: string) => {
|
|
960
|
+
return item[key] !== undefined
|
|
961
|
+
? String(item[key])
|
|
962
|
+
: match;
|
|
963
|
+
},
|
|
964
|
+
);
|
|
965
|
+
return itemTemplate;
|
|
966
|
+
})
|
|
967
|
+
.join("");
|
|
968
|
+
},
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
// Handle conditionals: {{#if condition}}content{{/if}}
|
|
972
|
+
rendered = rendered.replace(
|
|
973
|
+
/\{\{#if (\w+)\}\}(.*?)\{\{\/if\}\}/gs,
|
|
974
|
+
(match, conditionKey, content) => {
|
|
975
|
+
const condition = data[conditionKey];
|
|
976
|
+
return condition ? content : "";
|
|
977
|
+
},
|
|
978
|
+
);
|
|
979
|
+
|
|
980
|
+
// Handle layout
|
|
981
|
+
if (options.defaultLayout) {
|
|
982
|
+
const layoutPath = path.join(
|
|
983
|
+
options.views,
|
|
984
|
+
"layouts",
|
|
985
|
+
`${options.defaultLayout}.html`,
|
|
986
|
+
);
|
|
987
|
+
try {
|
|
988
|
+
let layoutContent: string;
|
|
989
|
+
|
|
990
|
+
if (options.cache && templateCache.has(layoutPath)) {
|
|
991
|
+
layoutContent = templateCache.get(layoutPath)!;
|
|
992
|
+
} else {
|
|
993
|
+
layoutContent = await fs.readFile(layoutPath, "utf-8");
|
|
994
|
+
if (options.cache) {
|
|
995
|
+
templateCache.set(layoutPath, layoutContent);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
rendered = layoutContent.replace(/\{\{body\}\}/, rendered);
|
|
1000
|
+
} catch (error) {
|
|
1001
|
+
// Layout not found, use template as-is
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
res.setHeader("Content-Type", "text/html");
|
|
1006
|
+
res.end(rendered);
|
|
1007
|
+
} catch (error) {
|
|
1008
|
+
res
|
|
1009
|
+
.status(500)
|
|
1010
|
+
.json({ success: false, error: "Template rendering failed" });
|
|
1011
|
+
}
|
|
1012
|
+
};
|
|
1013
|
+
|
|
1014
|
+
next();
|
|
1015
|
+
};
|
|
1016
|
+
},
|
|
1017
|
+
|
|
1018
|
+
// HTTP/2 Server Push middleware
|
|
1019
|
+
http2Push: (
|
|
1020
|
+
options: {
|
|
1021
|
+
resources?: Array<{ path: string; as: string; type?: string }>;
|
|
1022
|
+
condition?: (req: any) => boolean;
|
|
1023
|
+
} = {},
|
|
1024
|
+
): Middleware => {
|
|
1025
|
+
return (req, res, next) => {
|
|
1026
|
+
// Add HTTP/2 push capability to response
|
|
1027
|
+
(res as any).push = (path: string, options: any = {}) => {
|
|
1028
|
+
// Check if HTTP/2 is supported
|
|
1029
|
+
if (
|
|
1030
|
+
req.httpVersion === "2.0" &&
|
|
1031
|
+
(res as any).stream &&
|
|
1032
|
+
(res as any).stream.pushAllowed
|
|
1033
|
+
) {
|
|
1034
|
+
try {
|
|
1035
|
+
const pushStream = (res as any).stream.pushStream({
|
|
1036
|
+
":method": "GET",
|
|
1037
|
+
":path": path,
|
|
1038
|
+
...options.headers,
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
if (pushStream) {
|
|
1042
|
+
// Handle push stream
|
|
1043
|
+
return pushStream;
|
|
1044
|
+
}
|
|
1045
|
+
} catch (error) {
|
|
1046
|
+
// Push failed, continue normally
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
};
|
|
1051
|
+
|
|
1052
|
+
// Auto-push configured resources
|
|
1053
|
+
if (options.resources && (!options.condition || options.condition(req))) {
|
|
1054
|
+
for (const resource of options.resources) {
|
|
1055
|
+
(res as any).push?.(resource.path, {
|
|
1056
|
+
headers: {
|
|
1057
|
+
"content-type": resource.type || "text/plain",
|
|
1058
|
+
},
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
next();
|
|
1064
|
+
};
|
|
1065
|
+
},
|
|
1066
|
+
|
|
1067
|
+
// Server-Sent Events middleware
|
|
1068
|
+
sse: (
|
|
1069
|
+
options: {
|
|
1070
|
+
heartbeat?: number;
|
|
1071
|
+
retry?: number;
|
|
1072
|
+
cors?: boolean;
|
|
1073
|
+
} = {},
|
|
1074
|
+
): Middleware => {
|
|
1075
|
+
return (req, res, next) => {
|
|
1076
|
+
// Only handle SSE requests
|
|
1077
|
+
if (req.headers.accept?.includes("text/event-stream")) {
|
|
1078
|
+
// Set SSE headers
|
|
1079
|
+
res.writeHead(200, {
|
|
1080
|
+
"Content-Type": "text/event-stream",
|
|
1081
|
+
"Cache-Control": "no-cache",
|
|
1082
|
+
Connection: "keep-alive",
|
|
1083
|
+
"Access-Control-Allow-Origin": options.cors ? "*" : undefined,
|
|
1084
|
+
"Access-Control-Allow-Headers": options.cors
|
|
1085
|
+
? "Cache-Control"
|
|
1086
|
+
: undefined,
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// Add SSE methods to response
|
|
1090
|
+
(res as any).sendEvent = (data: any, event?: string, id?: string) => {
|
|
1091
|
+
if (id) res.write(`id: ${id}\n`);
|
|
1092
|
+
if (event) res.write(`event: ${event}\n`);
|
|
1093
|
+
res.write(
|
|
1094
|
+
`data: ${typeof data === "string" ? data : JSON.stringify(data)}\n\n`,
|
|
1095
|
+
);
|
|
1096
|
+
};
|
|
1097
|
+
|
|
1098
|
+
(res as any).sendComment = (comment: string) => {
|
|
1099
|
+
res.write(`: ${comment}\n\n`);
|
|
1100
|
+
};
|
|
1101
|
+
|
|
1102
|
+
(res as any).sendRetry = (ms: number) => {
|
|
1103
|
+
res.write(`retry: ${ms}\n\n`);
|
|
1104
|
+
};
|
|
1105
|
+
|
|
1106
|
+
// Set up heartbeat if configured
|
|
1107
|
+
let heartbeatInterval: NodeJS.Timeout | null = null;
|
|
1108
|
+
if (options.heartbeat) {
|
|
1109
|
+
heartbeatInterval = setInterval(() => {
|
|
1110
|
+
(res as any).sendComment("heartbeat");
|
|
1111
|
+
}, options.heartbeat);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
// Set retry if configured
|
|
1115
|
+
if (options.retry) {
|
|
1116
|
+
(res as any).sendRetry(options.retry);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Clean up on close
|
|
1120
|
+
req.on("close", () => {
|
|
1121
|
+
if (heartbeatInterval) {
|
|
1122
|
+
clearInterval(heartbeatInterval);
|
|
1123
|
+
}
|
|
1124
|
+
});
|
|
1125
|
+
|
|
1126
|
+
// Don't call next() - this middleware handles the response
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
next();
|
|
1131
|
+
};
|
|
1132
|
+
},
|
|
1133
|
+
|
|
1134
|
+
// Range request middleware for streaming
|
|
1135
|
+
range: (
|
|
1136
|
+
options: {
|
|
1137
|
+
acceptRanges?: string;
|
|
1138
|
+
maxRanges?: number;
|
|
1139
|
+
} = {},
|
|
1140
|
+
): Middleware => {
|
|
1141
|
+
return async (req, res, next) => {
|
|
1142
|
+
// Add range support to response
|
|
1143
|
+
(res as any).sendRange = async (filePath: string, stats?: any) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const fs = await import("fs/promises");
|
|
1146
|
+
const path = await import("path");
|
|
1147
|
+
|
|
1148
|
+
if (!stats) {
|
|
1149
|
+
stats = await fs.stat(filePath);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const fileSize = stats.size;
|
|
1153
|
+
const range = req.headers.range;
|
|
1154
|
+
|
|
1155
|
+
// Set Accept-Ranges header
|
|
1156
|
+
res.setHeader("Accept-Ranges", options.acceptRanges || "bytes");
|
|
1157
|
+
|
|
1158
|
+
if (!range) {
|
|
1159
|
+
// No range requested, send entire file
|
|
1160
|
+
res.setHeader("Content-Length", fileSize);
|
|
1161
|
+
const data = await fs.readFile(filePath);
|
|
1162
|
+
res.end(data);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// Parse range header
|
|
1167
|
+
const ranges = range
|
|
1168
|
+
.replace(/bytes=/, "")
|
|
1169
|
+
.split(",")
|
|
1170
|
+
.map((r) => {
|
|
1171
|
+
const [start, end] = r.split("-");
|
|
1172
|
+
return {
|
|
1173
|
+
start: start ? parseInt(start) : 0,
|
|
1174
|
+
end: end ? parseInt(end) : fileSize - 1,
|
|
1175
|
+
};
|
|
1176
|
+
});
|
|
1177
|
+
|
|
1178
|
+
// Validate ranges
|
|
1179
|
+
if (options.maxRanges && ranges.length > options.maxRanges) {
|
|
1180
|
+
res.status(416).json({ success: false, error: "Too many ranges" });
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (ranges.length === 1) {
|
|
1185
|
+
// Single range
|
|
1186
|
+
const { start, end } = ranges[0];
|
|
1187
|
+
const chunkSize = end - start + 1;
|
|
1188
|
+
|
|
1189
|
+
if (start >= fileSize || end >= fileSize) {
|
|
1190
|
+
res.status(416).setHeader("Content-Range", `bytes */${fileSize}`);
|
|
1191
|
+
res.json({ success: false, error: "Range not satisfiable" });
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
res.status(206);
|
|
1196
|
+
res.setHeader("Content-Range", `bytes ${start}-${end}/${fileSize}`);
|
|
1197
|
+
res.setHeader("Content-Length", chunkSize);
|
|
1198
|
+
|
|
1199
|
+
// Stream the range
|
|
1200
|
+
const stream = require("fs").createReadStream(filePath, {
|
|
1201
|
+
start,
|
|
1202
|
+
end,
|
|
1203
|
+
});
|
|
1204
|
+
stream.pipe(res);
|
|
1205
|
+
} else {
|
|
1206
|
+
// Multiple ranges - multipart response
|
|
1207
|
+
const boundary = "MULTIPART_BYTERANGES";
|
|
1208
|
+
res.status(206);
|
|
1209
|
+
res.setHeader(
|
|
1210
|
+
"Content-Type",
|
|
1211
|
+
`multipart/byteranges; boundary=${boundary}`,
|
|
1212
|
+
);
|
|
1213
|
+
|
|
1214
|
+
for (const { start, end } of ranges) {
|
|
1215
|
+
if (start >= fileSize || end >= fileSize) continue;
|
|
1216
|
+
|
|
1217
|
+
const chunkSize = end - start + 1;
|
|
1218
|
+
res.write(`\r\n--${boundary}\r\n`);
|
|
1219
|
+
res.write(
|
|
1220
|
+
`Content-Range: bytes ${start}-${end}/${fileSize}\r\n\r\n`,
|
|
1221
|
+
);
|
|
1222
|
+
|
|
1223
|
+
const stream = require("fs").createReadStream(filePath, {
|
|
1224
|
+
start,
|
|
1225
|
+
end,
|
|
1226
|
+
});
|
|
1227
|
+
await new Promise((resolve) => {
|
|
1228
|
+
stream.on("end", resolve);
|
|
1229
|
+
stream.pipe(res, { end: false });
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
res.write(`\r\n--${boundary}--\r\n`);
|
|
1233
|
+
res.end();
|
|
1234
|
+
}
|
|
1235
|
+
} catch (error) {
|
|
1236
|
+
res
|
|
1237
|
+
.status(500)
|
|
1238
|
+
.json({ success: false, error: "Range request failed" });
|
|
1239
|
+
}
|
|
1240
|
+
};
|
|
1241
|
+
|
|
1242
|
+
next();
|
|
1243
|
+
};
|
|
1244
|
+
},
|
|
1245
|
+
|
|
1246
|
+
// CSRF Protection middleware
|
|
1247
|
+
csrf: (
|
|
1248
|
+
options: {
|
|
1249
|
+
secret?: string;
|
|
1250
|
+
tokenLength?: number;
|
|
1251
|
+
cookieName?: string;
|
|
1252
|
+
headerName?: string;
|
|
1253
|
+
ignoreMethods?: string[];
|
|
1254
|
+
sameSite?: boolean;
|
|
1255
|
+
} = {},
|
|
1256
|
+
): Middleware => {
|
|
1257
|
+
const secret = options.secret || "moro-csrf-secret";
|
|
1258
|
+
const tokenLength = options.tokenLength || 32;
|
|
1259
|
+
const cookieName = options.cookieName || "_csrf";
|
|
1260
|
+
const headerName = options.headerName || "x-csrf-token";
|
|
1261
|
+
const ignoreMethods = options.ignoreMethods || ["GET", "HEAD", "OPTIONS"];
|
|
1262
|
+
|
|
1263
|
+
const generateToken = () => {
|
|
1264
|
+
const crypto = require("crypto");
|
|
1265
|
+
return crypto.randomBytes(tokenLength).toString("hex");
|
|
1266
|
+
};
|
|
1267
|
+
|
|
1268
|
+
const verifyToken = (token: string, sessionToken: string) => {
|
|
1269
|
+
return token && sessionToken && token === sessionToken;
|
|
1270
|
+
};
|
|
1271
|
+
|
|
1272
|
+
return (req, res, next) => {
|
|
1273
|
+
// Add CSRF token generation method
|
|
1274
|
+
(req as any).csrfToken = () => {
|
|
1275
|
+
if (!(req as any)._csrfToken) {
|
|
1276
|
+
(req as any)._csrfToken = generateToken();
|
|
1277
|
+
// Set token in cookie
|
|
1278
|
+
res.cookie(cookieName, (req as any)._csrfToken, {
|
|
1279
|
+
httpOnly: true,
|
|
1280
|
+
sameSite: options.sameSite !== false ? "strict" : undefined,
|
|
1281
|
+
secure:
|
|
1282
|
+
req.headers["x-forwarded-proto"] === "https" ||
|
|
1283
|
+
(req.socket as any).encrypted,
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
return (req as any)._csrfToken;
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// Skip verification for safe methods
|
|
1290
|
+
if (ignoreMethods.includes(req.method!)) {
|
|
1291
|
+
next();
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
// Get token from header or body
|
|
1296
|
+
const token =
|
|
1297
|
+
req.headers[headerName] ||
|
|
1298
|
+
(req.body && req.body._csrf) ||
|
|
1299
|
+
(req.query && req.query._csrf);
|
|
1300
|
+
|
|
1301
|
+
// Get session token from cookie
|
|
1302
|
+
const sessionToken = req.cookies?.[cookieName];
|
|
1303
|
+
|
|
1304
|
+
if (!verifyToken(token as string, sessionToken || "")) {
|
|
1305
|
+
res.status(403).json({
|
|
1306
|
+
success: false,
|
|
1307
|
+
error: "Invalid CSRF token",
|
|
1308
|
+
code: "CSRF_TOKEN_MISMATCH",
|
|
1309
|
+
});
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
next();
|
|
1314
|
+
};
|
|
1315
|
+
},
|
|
1316
|
+
|
|
1317
|
+
// Content Security Policy middleware
|
|
1318
|
+
csp: (
|
|
1319
|
+
options: {
|
|
1320
|
+
directives?: {
|
|
1321
|
+
defaultSrc?: string[];
|
|
1322
|
+
scriptSrc?: string[];
|
|
1323
|
+
styleSrc?: string[];
|
|
1324
|
+
imgSrc?: string[];
|
|
1325
|
+
connectSrc?: string[];
|
|
1326
|
+
fontSrc?: string[];
|
|
1327
|
+
objectSrc?: string[];
|
|
1328
|
+
mediaSrc?: string[];
|
|
1329
|
+
frameSrc?: string[];
|
|
1330
|
+
childSrc?: string[];
|
|
1331
|
+
workerSrc?: string[];
|
|
1332
|
+
formAction?: string[];
|
|
1333
|
+
upgradeInsecureRequests?: boolean;
|
|
1334
|
+
blockAllMixedContent?: boolean;
|
|
1335
|
+
};
|
|
1336
|
+
reportOnly?: boolean;
|
|
1337
|
+
reportUri?: string;
|
|
1338
|
+
nonce?: boolean;
|
|
1339
|
+
} = {},
|
|
1340
|
+
): Middleware => {
|
|
1341
|
+
return (req, res, next) => {
|
|
1342
|
+
const directives = options.directives || {
|
|
1343
|
+
defaultSrc: ["'self'"],
|
|
1344
|
+
scriptSrc: ["'self'"],
|
|
1345
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
1346
|
+
imgSrc: ["'self'", "data:", "https:"],
|
|
1347
|
+
connectSrc: ["'self'"],
|
|
1348
|
+
fontSrc: ["'self'"],
|
|
1349
|
+
objectSrc: ["'none'"],
|
|
1350
|
+
mediaSrc: ["'self'"],
|
|
1351
|
+
frameSrc: ["'none'"],
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
// Generate nonce if requested
|
|
1355
|
+
let nonce: string | undefined;
|
|
1356
|
+
if (options.nonce) {
|
|
1357
|
+
const crypto = require("crypto");
|
|
1358
|
+
nonce = crypto.randomBytes(16).toString("base64");
|
|
1359
|
+
(req as any).cspNonce = nonce;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// Build CSP header value
|
|
1363
|
+
const cspParts: string[] = [];
|
|
1364
|
+
|
|
1365
|
+
for (const [directive, sources] of Object.entries(directives)) {
|
|
1366
|
+
if (directive === "upgradeInsecureRequests" && sources === true) {
|
|
1367
|
+
cspParts.push("upgrade-insecure-requests");
|
|
1368
|
+
} else if (directive === "blockAllMixedContent" && sources === true) {
|
|
1369
|
+
cspParts.push("block-all-mixed-content");
|
|
1370
|
+
} else if (Array.isArray(sources)) {
|
|
1371
|
+
let sourceList = sources.join(" ");
|
|
1372
|
+
|
|
1373
|
+
// Add nonce to script-src and style-src if enabled
|
|
1374
|
+
if (
|
|
1375
|
+
nonce &&
|
|
1376
|
+
(directive === "scriptSrc" || directive === "styleSrc")
|
|
1377
|
+
) {
|
|
1378
|
+
sourceList += ` 'nonce-${nonce}'`;
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Convert camelCase to kebab-case
|
|
1382
|
+
const kebabDirective = directive
|
|
1383
|
+
.replace(/([A-Z])/g, "-$1")
|
|
1384
|
+
.toLowerCase();
|
|
1385
|
+
cspParts.push(`${kebabDirective} ${sourceList}`);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Add report-uri if specified
|
|
1390
|
+
if (options.reportUri) {
|
|
1391
|
+
cspParts.push(`report-uri ${options.reportUri}`);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const cspValue = cspParts.join("; ");
|
|
1395
|
+
const headerName = options.reportOnly
|
|
1396
|
+
? "Content-Security-Policy-Report-Only"
|
|
1397
|
+
: "Content-Security-Policy";
|
|
1398
|
+
|
|
1399
|
+
res.setHeader(headerName, cspValue);
|
|
1400
|
+
|
|
1401
|
+
next();
|
|
1402
|
+
};
|
|
1403
|
+
},
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1406
|
+
function parseSize(size: string): number {
|
|
1407
|
+
const units: { [key: string]: number } = {
|
|
1408
|
+
b: 1,
|
|
1409
|
+
kb: 1024,
|
|
1410
|
+
mb: 1024 * 1024,
|
|
1411
|
+
gb: 1024 * 1024 * 1024,
|
|
1412
|
+
};
|
|
1413
|
+
|
|
1414
|
+
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
|
|
1415
|
+
if (!match) return 1024 * 1024; // Default 1MB
|
|
1416
|
+
|
|
1417
|
+
const value = parseFloat(match[1]);
|
|
1418
|
+
const unit = match[2] || "b";
|
|
1419
|
+
|
|
1420
|
+
return Math.round(value * units[unit]);
|
|
1421
|
+
}
|