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