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