@morojs/moro 1.6.8 → 1.7.1
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 +3 -1
- package/dist/core/auth/morojs-adapter.js +16 -6
- package/dist/core/auth/morojs-adapter.js.map +1 -1
- package/dist/core/config/config-sources.js +27 -15
- package/dist/core/config/config-sources.js.map +1 -1
- package/dist/core/config/config-validator.js +201 -6
- package/dist/core/config/config-validator.js.map +1 -1
- package/dist/core/docs/openapi-generator.js +8 -9
- package/dist/core/docs/openapi-generator.js.map +1 -1
- package/dist/core/events/event-bus.js +1 -1
- package/dist/core/events/event-bus.js.map +1 -1
- package/dist/core/framework.d.ts +4 -2
- package/dist/core/framework.js +25 -24
- package/dist/core/framework.js.map +1 -1
- package/dist/core/graphql/core.js +34 -8
- package/dist/core/graphql/core.js.map +1 -1
- package/dist/core/grpc/adapters/grpc-js-adapter.d.ts +28 -0
- package/dist/core/grpc/adapters/grpc-js-adapter.js +449 -0
- package/dist/core/grpc/adapters/grpc-js-adapter.js.map +1 -0
- package/dist/core/grpc/adapters/index.d.ts +1 -0
- package/dist/core/grpc/adapters/index.js +6 -0
- package/dist/core/grpc/adapters/index.js.map +1 -0
- package/dist/core/grpc/grpc-adapter.d.ts +47 -0
- package/dist/core/grpc/grpc-adapter.js +4 -0
- package/dist/core/grpc/grpc-adapter.js.map +1 -0
- package/dist/core/grpc/grpc-manager.d.ts +59 -0
- package/dist/core/grpc/grpc-manager.js +218 -0
- package/dist/core/grpc/grpc-manager.js.map +1 -0
- package/dist/core/grpc/index.d.ts +7 -0
- package/dist/core/grpc/index.js +10 -0
- package/dist/core/grpc/index.js.map +1 -0
- package/dist/core/grpc/middleware/auth.d.ts +22 -0
- package/dist/core/grpc/middleware/auth.js +126 -0
- package/dist/core/grpc/middleware/auth.js.map +1 -0
- package/dist/core/grpc/middleware/logging.d.ts +19 -0
- package/dist/core/grpc/middleware/logging.js +57 -0
- package/dist/core/grpc/middleware/logging.js.map +1 -0
- package/dist/core/grpc/middleware/validation.d.ts +18 -0
- package/dist/core/grpc/middleware/validation.js +126 -0
- package/dist/core/grpc/middleware/validation.js.map +1 -0
- package/dist/core/grpc/types.d.ts +233 -0
- package/dist/core/grpc/types.js +36 -0
- package/dist/core/grpc/types.js.map +1 -0
- package/dist/core/http/http-server.d.ts +13 -84
- package/dist/core/http/http-server.js +338 -792
- package/dist/core/http/http-server.js.map +1 -1
- package/dist/core/http/http2-server.d.ts +131 -0
- package/dist/core/http/http2-server.js +936 -0
- package/dist/core/http/http2-server.js.map +1 -0
- package/dist/core/http/index.d.ts +3 -1
- package/dist/core/http/index.js +2 -1
- package/dist/core/http/index.js.map +1 -1
- package/dist/core/http/uws-http-server.js +154 -26
- package/dist/core/http/uws-http-server.js.map +1 -1
- package/dist/core/jobs/job-executor.js +6 -1
- package/dist/core/jobs/job-executor.js.map +1 -1
- package/dist/core/jobs/job-scheduler.js +4 -1
- package/dist/core/jobs/job-scheduler.js.map +1 -1
- package/dist/core/jobs/leader-election.js +2 -1
- package/dist/core/jobs/leader-election.js.map +1 -1
- package/dist/core/logger/logger.js +41 -18
- package/dist/core/logger/logger.js.map +1 -1
- package/dist/core/logger/outputs.js +9 -3
- package/dist/core/logger/outputs.js.map +1 -1
- package/dist/core/mail/adapters/console-adapter.d.ts +14 -0
- package/dist/core/mail/adapters/console-adapter.js +89 -0
- package/dist/core/mail/adapters/console-adapter.js.map +1 -0
- package/dist/core/mail/adapters/index.d.ts +5 -0
- package/dist/core/mail/adapters/index.js +8 -0
- package/dist/core/mail/adapters/index.js.map +1 -0
- package/dist/core/mail/adapters/nodemailer-adapter.d.ts +18 -0
- package/dist/core/mail/adapters/nodemailer-adapter.js +188 -0
- package/dist/core/mail/adapters/nodemailer-adapter.js.map +1 -0
- package/dist/core/mail/adapters/resend-adapter.d.ts +18 -0
- package/dist/core/mail/adapters/resend-adapter.js +169 -0
- package/dist/core/mail/adapters/resend-adapter.js.map +1 -0
- package/dist/core/mail/adapters/sendgrid-adapter.d.ts +19 -0
- package/dist/core/mail/adapters/sendgrid-adapter.js +186 -0
- package/dist/core/mail/adapters/sendgrid-adapter.js.map +1 -0
- package/dist/core/mail/adapters/ses-adapter.d.ts +18 -0
- package/dist/core/mail/adapters/ses-adapter.js +167 -0
- package/dist/core/mail/adapters/ses-adapter.js.map +1 -0
- package/dist/core/mail/index.d.ts +5 -0
- package/dist/core/mail/index.js +8 -0
- package/dist/core/mail/index.js.map +1 -0
- package/dist/core/mail/mail-adapter.d.ts +62 -0
- package/dist/core/mail/mail-adapter.js +83 -0
- package/dist/core/mail/mail-adapter.js.map +1 -0
- package/dist/core/mail/mail-manager.d.ts +63 -0
- package/dist/core/mail/mail-manager.js +302 -0
- package/dist/core/mail/mail-manager.js.map +1 -0
- package/dist/core/mail/template-engine.d.ts +43 -0
- package/dist/core/mail/template-engine.js +239 -0
- package/dist/core/mail/template-engine.js.map +1 -0
- package/dist/core/mail/types.d.ts +237 -0
- package/dist/core/mail/types.js +4 -0
- package/dist/core/mail/types.js.map +1 -0
- package/dist/core/middleware/built-in/body-size/core.d.ts +12 -0
- package/dist/core/middleware/built-in/body-size/core.js +52 -0
- package/dist/core/middleware/built-in/body-size/core.js.map +1 -0
- package/dist/core/middleware/built-in/body-size/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/body-size/hook.js +12 -0
- package/dist/core/middleware/built-in/body-size/hook.js.map +1 -0
- package/dist/core/middleware/built-in/body-size/index.d.ts +6 -0
- package/dist/core/middleware/built-in/body-size/index.js +7 -0
- package/dist/core/middleware/built-in/body-size/index.js.map +1 -0
- package/dist/core/middleware/built-in/body-size/middleware.d.ts +14 -0
- package/dist/core/middleware/built-in/body-size/middleware.js +22 -0
- package/dist/core/middleware/built-in/body-size/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/cache/core.d.ts +20 -1
- package/dist/core/middleware/built-in/cache/core.js.map +1 -1
- package/dist/core/middleware/built-in/cache/hook.d.ts +38 -1
- package/dist/core/middleware/built-in/cache/hook.js +202 -16
- package/dist/core/middleware/built-in/cache/hook.js.map +1 -1
- package/dist/core/middleware/built-in/cache/index.js +1 -1
- package/dist/core/middleware/built-in/cache/index.js.map +1 -1
- package/dist/core/middleware/built-in/compression/core.d.ts +16 -0
- package/dist/core/middleware/built-in/compression/core.js +75 -0
- package/dist/core/middleware/built-in/compression/core.js.map +1 -0
- package/dist/core/middleware/built-in/compression/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/compression/hook.js +14 -0
- package/dist/core/middleware/built-in/compression/hook.js.map +1 -0
- package/dist/core/middleware/built-in/compression/index.d.ts +6 -0
- package/dist/core/middleware/built-in/compression/index.js +7 -0
- package/dist/core/middleware/built-in/compression/index.js.map +1 -0
- package/dist/core/middleware/built-in/compression/middleware.d.ts +20 -0
- package/dist/core/middleware/built-in/compression/middleware.js +28 -0
- package/dist/core/middleware/built-in/compression/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/cookie/core.js +37 -9
- package/dist/core/middleware/built-in/cookie/core.js.map +1 -1
- package/dist/core/middleware/built-in/helmet/core.d.ts +19 -0
- package/dist/core/middleware/built-in/helmet/core.js +70 -0
- package/dist/core/middleware/built-in/helmet/core.js.map +1 -0
- package/dist/core/middleware/built-in/helmet/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/helmet/hook.js +12 -0
- package/dist/core/middleware/built-in/helmet/hook.js.map +1 -0
- package/dist/core/middleware/built-in/helmet/index.d.ts +6 -0
- package/dist/core/middleware/built-in/helmet/index.js +7 -0
- package/dist/core/middleware/built-in/helmet/index.js.map +1 -0
- package/dist/core/middleware/built-in/helmet/middleware.d.ts +22 -0
- package/dist/core/middleware/built-in/helmet/middleware.js +28 -0
- package/dist/core/middleware/built-in/helmet/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/http2/core.d.ts +35 -0
- package/dist/core/middleware/built-in/http2/core.js +128 -0
- package/dist/core/middleware/built-in/http2/core.js.map +1 -0
- package/dist/core/middleware/built-in/http2/hook.d.ts +5 -0
- package/dist/core/middleware/built-in/http2/hook.js +34 -0
- package/dist/core/middleware/built-in/http2/hook.js.map +1 -0
- package/dist/core/middleware/built-in/http2/index.d.ts +8 -0
- package/dist/core/middleware/built-in/http2/index.js +10 -0
- package/dist/core/middleware/built-in/http2/index.js.map +1 -0
- package/dist/core/middleware/built-in/http2/middleware.d.ts +20 -0
- package/dist/core/middleware/built-in/http2/middleware.js +31 -0
- package/dist/core/middleware/built-in/http2/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/index.d.ts +18 -0
- package/dist/core/middleware/built-in/index.js +28 -0
- package/dist/core/middleware/built-in/index.js.map +1 -1
- package/dist/core/middleware/built-in/range/core.d.ts +16 -0
- package/dist/core/middleware/built-in/range/core.js +112 -0
- package/dist/core/middleware/built-in/range/core.js.map +1 -0
- package/dist/core/middleware/built-in/range/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/range/hook.js +12 -0
- package/dist/core/middleware/built-in/range/hook.js.map +1 -0
- package/dist/core/middleware/built-in/range/index.d.ts +6 -0
- package/dist/core/middleware/built-in/range/index.js +7 -0
- package/dist/core/middleware/built-in/range/index.js.map +1 -0
- package/dist/core/middleware/built-in/range/middleware.d.ts +21 -0
- package/dist/core/middleware/built-in/range/middleware.js +27 -0
- package/dist/core/middleware/built-in/range/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/session/core.js +14 -1
- package/dist/core/middleware/built-in/session/core.js.map +1 -1
- package/dist/core/middleware/built-in/static/core.d.ts +20 -0
- package/dist/core/middleware/built-in/static/core.js +143 -0
- package/dist/core/middleware/built-in/static/core.js.map +1 -0
- package/dist/core/middleware/built-in/static/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/static/hook.js +12 -0
- package/dist/core/middleware/built-in/static/hook.js.map +1 -0
- package/dist/core/middleware/built-in/static/index.d.ts +6 -0
- package/dist/core/middleware/built-in/static/index.js +7 -0
- package/dist/core/middleware/built-in/static/index.js.map +1 -0
- package/dist/core/middleware/built-in/static/middleware.d.ts +18 -0
- package/dist/core/middleware/built-in/static/middleware.js +26 -0
- package/dist/core/middleware/built-in/static/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/template/core.d.ts +19 -0
- package/dist/core/middleware/built-in/template/core.js +108 -0
- package/dist/core/middleware/built-in/template/core.js.map +1 -0
- package/dist/core/middleware/built-in/template/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/template/hook.js +12 -0
- package/dist/core/middleware/built-in/template/hook.js.map +1 -0
- package/dist/core/middleware/built-in/template/index.d.ts +6 -0
- package/dist/core/middleware/built-in/template/index.js +7 -0
- package/dist/core/middleware/built-in/template/index.js.map +1 -0
- package/dist/core/middleware/built-in/template/middleware.d.ts +21 -0
- package/dist/core/middleware/built-in/template/middleware.js +27 -0
- package/dist/core/middleware/built-in/template/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/upload/core.d.ts +29 -0
- package/dist/core/middleware/built-in/upload/core.js +66 -0
- package/dist/core/middleware/built-in/upload/core.js.map +1 -0
- package/dist/core/middleware/built-in/upload/hook.d.ts +2 -0
- package/dist/core/middleware/built-in/upload/hook.js +25 -0
- package/dist/core/middleware/built-in/upload/hook.js.map +1 -0
- package/dist/core/middleware/built-in/upload/index.d.ts +6 -0
- package/dist/core/middleware/built-in/upload/index.js +7 -0
- package/dist/core/middleware/built-in/upload/index.js.map +1 -0
- package/dist/core/middleware/built-in/upload/middleware.d.ts +18 -0
- package/dist/core/middleware/built-in/upload/middleware.js +41 -0
- package/dist/core/middleware/built-in/upload/middleware.js.map +1 -0
- package/dist/core/middleware/built-in/validation/middleware.js +2 -1
- package/dist/core/middleware/built-in/validation/middleware.js.map +1 -1
- package/dist/core/networking/adapters/uws-adapter.js +54 -6
- package/dist/core/networking/adapters/uws-adapter.js.map +1 -1
- package/dist/core/networking/adapters/ws-adapter.js +56 -17
- package/dist/core/networking/adapters/ws-adapter.js.map +1 -1
- package/dist/core/pooling/object-pool-manager.js +9 -1
- package/dist/core/pooling/object-pool-manager.js.map +1 -1
- package/dist/core/queue/adapters/bull-adapter.d.ts +86 -0
- package/dist/core/queue/adapters/bull-adapter.js +330 -0
- package/dist/core/queue/adapters/bull-adapter.js.map +1 -0
- package/dist/core/queue/adapters/index.d.ts +9 -0
- package/dist/core/queue/adapters/index.js +10 -0
- package/dist/core/queue/adapters/index.js.map +1 -0
- package/dist/core/queue/adapters/kafka-adapter.d.ts +86 -0
- package/dist/core/queue/adapters/kafka-adapter.js +462 -0
- package/dist/core/queue/adapters/kafka-adapter.js.map +1 -0
- package/dist/core/queue/adapters/memory-adapter.d.ts +87 -0
- package/dist/core/queue/adapters/memory-adapter.js +419 -0
- package/dist/core/queue/adapters/memory-adapter.js.map +1 -0
- package/dist/core/queue/adapters/rabbitmq-adapter.d.ts +86 -0
- package/dist/core/queue/adapters/rabbitmq-adapter.js +436 -0
- package/dist/core/queue/adapters/rabbitmq-adapter.js.map +1 -0
- package/dist/core/queue/adapters/sqs-adapter.d.ts +102 -0
- package/dist/core/queue/adapters/sqs-adapter.js +522 -0
- package/dist/core/queue/adapters/sqs-adapter.js.map +1 -0
- package/dist/core/queue/index.d.ts +11 -0
- package/dist/core/queue/index.js +14 -0
- package/dist/core/queue/index.js.map +1 -0
- package/dist/core/queue/middleware/index.d.ts +7 -0
- package/dist/core/queue/middleware/index.js +8 -0
- package/dist/core/queue/middleware/index.js.map +1 -0
- package/dist/core/queue/middleware/monitoring.d.ts +84 -0
- package/dist/core/queue/middleware/monitoring.js +145 -0
- package/dist/core/queue/middleware/monitoring.js.map +1 -0
- package/dist/core/queue/middleware/priority.d.ts +61 -0
- package/dist/core/queue/middleware/priority.js +90 -0
- package/dist/core/queue/middleware/priority.js.map +1 -0
- package/dist/core/queue/middleware/rate-limit.d.ts +34 -0
- package/dist/core/queue/middleware/rate-limit.js +109 -0
- package/dist/core/queue/middleware/rate-limit.js.map +1 -0
- package/dist/core/queue/queue-adapter.d.ts +73 -0
- package/dist/core/queue/queue-adapter.js +20 -0
- package/dist/core/queue/queue-adapter.js.map +1 -0
- package/dist/core/queue/queue-manager.d.ts +92 -0
- package/dist/core/queue/queue-manager.js +327 -0
- package/dist/core/queue/queue-manager.js.map +1 -0
- package/dist/core/queue/types.d.ts +205 -0
- package/dist/core/queue/types.js +6 -0
- package/dist/core/queue/types.js.map +1 -0
- package/dist/core/routing/index.js +41 -10
- package/dist/core/routing/index.js.map +1 -1
- package/dist/core/routing/radix-tree.d.ts +48 -0
- package/dist/core/routing/radix-tree.js +211 -0
- package/dist/core/routing/radix-tree.js.map +1 -0
- package/dist/core/routing/router.d.ts +10 -9
- package/dist/core/routing/router.js +3 -1
- package/dist/core/routing/router.js.map +1 -1
- package/dist/core/routing/unified-router.d.ts +18 -12
- package/dist/core/routing/unified-router.js +220 -163
- package/dist/core/routing/unified-router.js.map +1 -1
- package/dist/core/runtime/aws-lambda-adapter.js +21 -10
- package/dist/core/runtime/aws-lambda-adapter.js.map +1 -1
- package/dist/core/runtime/base-adapter.js +15 -5
- package/dist/core/runtime/base-adapter.js.map +1 -1
- package/dist/core/runtime/cloudflare-workers-adapter.js +35 -12
- package/dist/core/runtime/cloudflare-workers-adapter.js.map +1 -1
- package/dist/core/runtime/vercel-edge-adapter.js +16 -6
- package/dist/core/runtime/vercel-edge-adapter.js.map +1 -1
- package/dist/core/utilities/container.js +3 -1
- package/dist/core/utilities/container.js.map +1 -1
- package/dist/core/utilities/index.d.ts +2 -0
- package/dist/core/utilities/index.js +2 -0
- package/dist/core/utilities/index.js.map +1 -1
- package/dist/core/utilities/response-helpers.d.ts +280 -0
- package/dist/core/utilities/response-helpers.js +359 -0
- package/dist/core/utilities/response-helpers.js.map +1 -0
- package/dist/core/workers/facade.d.ts +74 -0
- package/dist/core/workers/facade.js +98 -0
- package/dist/core/workers/facade.js.map +1 -0
- package/dist/core/workers/index.d.ts +2 -0
- package/dist/core/workers/index.js +6 -0
- package/dist/core/workers/index.js.map +1 -0
- package/dist/core/workers/worker-manager.d.ts +124 -0
- package/dist/core/workers/worker-manager.js +299 -0
- package/dist/core/workers/worker-manager.js.map +1 -0
- package/dist/core/workers/worker.d.ts +1 -0
- package/dist/core/workers/worker.js +225 -0
- package/dist/core/workers/worker.js.map +1 -0
- package/dist/index.d.ts +10 -2
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/dist/moro.d.ts +345 -13
- package/dist/moro.js +820 -221
- package/dist/moro.js.map +1 -1
- package/dist/types/cache.d.ts +4 -0
- package/dist/types/config.d.ts +42 -0
- package/dist/types/core.d.ts +18 -1
- package/dist/types/http.d.ts +21 -0
- package/package.json +98 -24
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// src/core/http-server.ts
|
|
2
2
|
import { createServer } from 'http';
|
|
3
3
|
import * as zlib from 'zlib';
|
|
4
|
-
import { createReadStream } from 'fs';
|
|
5
|
-
import * as crypto from 'crypto';
|
|
6
4
|
import { promisify } from 'util';
|
|
7
5
|
import { createFrameworkLogger } from '../logger/index.js';
|
|
8
6
|
import { PathMatcher } from '../routing/path-matcher.js';
|
|
@@ -38,44 +36,6 @@ export class MoroHttpServer {
|
|
|
38
36
|
methodNotAllowed: Buffer.from('{"success":false,"error":"Method not allowed"}'),
|
|
39
37
|
rateLimited: Buffer.from('{"success":false,"error":"Rate limit exceeded"}'),
|
|
40
38
|
};
|
|
41
|
-
// Buffer pool for zero-copy operations
|
|
42
|
-
static BUFFER_SIZES = [64, 256, 1024, 4096, 16384];
|
|
43
|
-
static BUFFER_POOLS = new Map();
|
|
44
|
-
static {
|
|
45
|
-
// Pre-allocate buffer pools for zero-allocation responses
|
|
46
|
-
for (const size of MoroHttpServer.BUFFER_SIZES) {
|
|
47
|
-
MoroHttpServer.BUFFER_POOLS.set(size, []);
|
|
48
|
-
for (let i = 0; i < 50; i++) {
|
|
49
|
-
// 50 buffers per size
|
|
50
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
51
|
-
MoroHttpServer.BUFFER_POOLS.get(size).push(Buffer.allocUnsafe(size));
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
static getOptimalBuffer(size) {
|
|
56
|
-
// Find the smallest buffer that fits
|
|
57
|
-
for (const poolSize of MoroHttpServer.BUFFER_SIZES) {
|
|
58
|
-
if (size <= poolSize) {
|
|
59
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
60
|
-
const pool = MoroHttpServer.BUFFER_POOLS.get(poolSize);
|
|
61
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
62
|
-
return pool.length > 0 ? pool.pop() : Buffer.allocUnsafe(poolSize);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
return Buffer.allocUnsafe(size);
|
|
66
|
-
}
|
|
67
|
-
static returnBuffer(buffer) {
|
|
68
|
-
// Return buffer to appropriate pool
|
|
69
|
-
const size = buffer.length;
|
|
70
|
-
if (MoroHttpServer.BUFFER_POOLS.has(size)) {
|
|
71
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
72
|
-
const pool = MoroHttpServer.BUFFER_POOLS.get(size);
|
|
73
|
-
if (pool.length < 50) {
|
|
74
|
-
// Don't let pools grow too large
|
|
75
|
-
pool.push(buffer);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
39
|
constructor() {
|
|
80
40
|
this.server = createServer(this.handleRequest.bind(this));
|
|
81
41
|
// Optimize server for high performance (conservative settings for compatibility)
|
|
@@ -289,9 +249,9 @@ export class MoroHttpServer {
|
|
|
289
249
|
}
|
|
290
250
|
}
|
|
291
251
|
finally {
|
|
292
|
-
//
|
|
252
|
+
// Always release pooled objects back to the pool
|
|
293
253
|
// This prevents memory leaks and ensures consistent performance
|
|
294
|
-
//
|
|
254
|
+
// Check if object is empty without Object.keys()
|
|
295
255
|
if (originalParams) {
|
|
296
256
|
let isEmpty = true;
|
|
297
257
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -317,7 +277,7 @@ export class MoroHttpServer {
|
|
|
317
277
|
}
|
|
318
278
|
// Additional cleanup on response completion to ensure objects are returned to pool
|
|
319
279
|
res.once('finish', () => {
|
|
320
|
-
//
|
|
280
|
+
// Check if object is empty without Object.keys()
|
|
321
281
|
if (originalParams) {
|
|
322
282
|
let isEmpty = true;
|
|
323
283
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -367,14 +327,40 @@ export class MoroHttpServer {
|
|
|
367
327
|
streamLargeResponse(res, data) {
|
|
368
328
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
369
329
|
res.setHeader('Transfer-Encoding', 'chunked');
|
|
370
|
-
// Stream
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
330
|
+
// Stream large JSON responses to prevent memory issues
|
|
331
|
+
if (Array.isArray(data) && data.length > 100) {
|
|
332
|
+
// Stream large arrays element by element
|
|
333
|
+
res.write('[');
|
|
334
|
+
// Stream each array element
|
|
335
|
+
let first = true;
|
|
336
|
+
for (const item of data) {
|
|
337
|
+
if (!first)
|
|
338
|
+
res.write(',');
|
|
339
|
+
res.write(JSON.stringify(item));
|
|
340
|
+
first = false;
|
|
341
|
+
}
|
|
342
|
+
// Write closing bracket and end
|
|
343
|
+
res.end(']');
|
|
344
|
+
}
|
|
345
|
+
else if (typeof data === 'object' && data !== null && Object.keys(data).length > 50) {
|
|
346
|
+
// For large objects, stream key-value pairs
|
|
347
|
+
res.write('{');
|
|
348
|
+
const keys = Object.keys(data);
|
|
349
|
+
let first = true;
|
|
350
|
+
for (const key of keys) {
|
|
351
|
+
if (!first)
|
|
352
|
+
res.write(',');
|
|
353
|
+
// Properly escape the key using JSON.stringify
|
|
354
|
+
res.write(`${JSON.stringify(key)}:${JSON.stringify(data[key])}`);
|
|
355
|
+
first = false;
|
|
356
|
+
}
|
|
357
|
+
res.end('}');
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
// For smaller data, still avoid the old chunking approach
|
|
361
|
+
const jsonString = JSON.stringify(data);
|
|
362
|
+
res.end(jsonString);
|
|
376
363
|
}
|
|
377
|
-
res.end();
|
|
378
364
|
}
|
|
379
365
|
normalizePath(path) {
|
|
380
366
|
// Check cache first
|
|
@@ -472,76 +458,21 @@ export class MoroHttpServer {
|
|
|
472
458
|
httpRes.json = async (data) => {
|
|
473
459
|
if (httpRes.headersSent)
|
|
474
460
|
return;
|
|
475
|
-
// JSON serialization
|
|
476
|
-
|
|
477
|
-
//
|
|
478
|
-
|
|
479
|
-
if (data && typeof data === 'object' && 'success' in data) {
|
|
480
|
-
// Check for common patterns using 'in' operator (faster than Object.keys for small objects)
|
|
481
|
-
const hasData = 'data' in data;
|
|
482
|
-
const hasError = 'error' in data;
|
|
483
|
-
const hasTotal = 'total' in data;
|
|
484
|
-
// Fast path: {success, data} - most common pattern
|
|
485
|
-
if (hasData && !hasError && !hasTotal) {
|
|
486
|
-
// Verify it's exactly 2 keys by checking no other common keys exist
|
|
487
|
-
if (!('message' in data) && !('code' in data) && !('status' in data)) {
|
|
488
|
-
jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)}}`;
|
|
489
|
-
}
|
|
490
|
-
else {
|
|
491
|
-
jsonString = JSON.stringify(data);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
else if (hasError && !hasData && !hasTotal) {
|
|
495
|
-
// Fast path: {success, error}
|
|
496
|
-
if (!('message' in data) && !('code' in data) && !('status' in data)) {
|
|
497
|
-
jsonString = `{"success":${data.success},"error":${JSON.stringify(data.error)}}`;
|
|
498
|
-
}
|
|
499
|
-
else {
|
|
500
|
-
jsonString = JSON.stringify(data);
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
else if (hasData && hasError && !hasTotal) {
|
|
504
|
-
// Fast path: {success, data, error}
|
|
505
|
-
if (!('message' in data) && !('code' in data) && !('status' in data)) {
|
|
506
|
-
jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)},"error":${JSON.stringify(data.error)}}`;
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
jsonString = JSON.stringify(data);
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
else if (hasData && hasTotal && !hasError) {
|
|
513
|
-
// Fast path: {success, data, total}
|
|
514
|
-
if (!('message' in data) && !('code' in data) && !('status' in data)) {
|
|
515
|
-
jsonString = `{"success":${data.success},"data":${JSON.stringify(data.data)},"total":${data.total}}`;
|
|
516
|
-
}
|
|
517
|
-
else {
|
|
518
|
-
jsonString = JSON.stringify(data);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
else {
|
|
522
|
-
// Complex object - use standard JSON.stringify
|
|
523
|
-
jsonString = JSON.stringify(data);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
else {
|
|
527
|
-
jsonString = JSON.stringify(data);
|
|
528
|
-
}
|
|
529
|
-
// Use buffer pool for zero-allocation responses
|
|
530
|
-
const estimatedSize = jsonString.length;
|
|
531
|
-
if (estimatedSize > 32768) {
|
|
461
|
+
// Simple, optimized JSON serialization - let V8 handle the optimization
|
|
462
|
+
const jsonString = JSON.stringify(data);
|
|
463
|
+
// Large response check - stream if needed
|
|
464
|
+
if (jsonString.length > 32768) {
|
|
532
465
|
// Large response - stream it
|
|
533
466
|
return this.streamLargeResponse(httpRes, data);
|
|
534
467
|
}
|
|
535
|
-
|
|
536
|
-
const
|
|
537
|
-
// Slice to actual size to avoid sending extra bytes
|
|
538
|
-
const finalBuffer = actualLength === buffer.length ? buffer : buffer.subarray(0, actualLength);
|
|
468
|
+
// Use efficient buffer allocation - let Node.js handle optimization
|
|
469
|
+
const finalBuffer = Buffer.from(jsonString, 'utf8');
|
|
539
470
|
// Optimized header setting - set multiple headers at once when possible
|
|
540
471
|
const headers = {
|
|
541
472
|
'Content-Type': 'application/json; charset=utf-8',
|
|
542
473
|
};
|
|
543
474
|
// Compression with buffer pool - EARLY EXIT if disabled or below threshold
|
|
544
|
-
//
|
|
475
|
+
// Only make this async if compression is actually happening
|
|
545
476
|
if (this.compressionEnabled && finalBuffer.length > this.compressionThreshold) {
|
|
546
477
|
const acceptEncoding = httpRes.req.headers['accept-encoding'];
|
|
547
478
|
if (acceptEncoding && acceptEncoding.includes('gzip')) {
|
|
@@ -552,8 +483,6 @@ export class MoroHttpServer {
|
|
|
552
483
|
// Batch write all headers at once (50-100% faster)
|
|
553
484
|
httpRes.writeHead(httpRes.statusCode || 200, headers);
|
|
554
485
|
httpRes.end(compressed);
|
|
555
|
-
// Return buffer to pool after response
|
|
556
|
-
process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
|
|
557
486
|
});
|
|
558
487
|
return;
|
|
559
488
|
}
|
|
@@ -565,8 +494,6 @@ export class MoroHttpServer {
|
|
|
565
494
|
// Batch write all headers at once
|
|
566
495
|
httpRes.writeHead(httpRes.statusCode || 200, headers);
|
|
567
496
|
httpRes.end(compressed);
|
|
568
|
-
// Return buffer to pool after response
|
|
569
|
-
process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
|
|
570
497
|
});
|
|
571
498
|
return;
|
|
572
499
|
}
|
|
@@ -576,8 +503,6 @@ export class MoroHttpServer {
|
|
|
576
503
|
// Batch write all headers at once
|
|
577
504
|
httpRes.writeHead(httpRes.statusCode || 200, headers);
|
|
578
505
|
httpRes.end(finalBuffer);
|
|
579
|
-
// Return buffer to pool after response (zero-copy achievement!)
|
|
580
|
-
process.nextTick(() => MoroHttpServer.returnBuffer(buffer));
|
|
581
506
|
};
|
|
582
507
|
httpRes.send = (data) => {
|
|
583
508
|
if (httpRes.headersSent)
|
|
@@ -638,15 +563,31 @@ export class MoroHttpServer {
|
|
|
638
563
|
if (options.path)
|
|
639
564
|
cookieString += `; Path=${options.path}`;
|
|
640
565
|
const existingCookies = httpRes.getHeader('Set-Cookie') || [];
|
|
566
|
+
// Avoid spread operator - direct array manipulation
|
|
641
567
|
const cookies = Array.isArray(existingCookies)
|
|
642
|
-
?
|
|
568
|
+
? existingCookies
|
|
643
569
|
: [existingCookies];
|
|
644
570
|
cookies.push(cookieString);
|
|
645
571
|
httpRes.setHeader('Set-Cookie', cookies);
|
|
646
572
|
return httpRes;
|
|
647
573
|
};
|
|
648
574
|
httpRes.clearCookie = (name, options = {}) => {
|
|
649
|
-
|
|
575
|
+
// Avoid spread operator - manually set properties
|
|
576
|
+
const clearOptions = {
|
|
577
|
+
expires: new Date(0),
|
|
578
|
+
maxAge: 0,
|
|
579
|
+
};
|
|
580
|
+
// Copy other options manually
|
|
581
|
+
if (options.path !== undefined)
|
|
582
|
+
clearOptions.path = options.path;
|
|
583
|
+
if (options.domain !== undefined)
|
|
584
|
+
clearOptions.domain = options.domain;
|
|
585
|
+
if (options.httpOnly !== undefined)
|
|
586
|
+
clearOptions.httpOnly = options.httpOnly;
|
|
587
|
+
if (options.secure !== undefined)
|
|
588
|
+
clearOptions.secure = options.secure;
|
|
589
|
+
if (options.sameSite !== undefined)
|
|
590
|
+
clearOptions.sameSite = options.sameSite;
|
|
650
591
|
return httpRes.cookie(name, '', clearOptions);
|
|
651
592
|
};
|
|
652
593
|
httpRes.redirect = (url, status = 302) => {
|
|
@@ -681,6 +622,139 @@ export class MoroHttpServer {
|
|
|
681
622
|
httpRes.status(404).json({ success: false, error: 'File not found' });
|
|
682
623
|
}
|
|
683
624
|
};
|
|
625
|
+
// Standardized response helpers
|
|
626
|
+
httpRes.success = (data, message) => {
|
|
627
|
+
const response = {
|
|
628
|
+
success: true,
|
|
629
|
+
data,
|
|
630
|
+
};
|
|
631
|
+
if (message !== undefined) {
|
|
632
|
+
response.message = message;
|
|
633
|
+
}
|
|
634
|
+
httpRes.json(response);
|
|
635
|
+
};
|
|
636
|
+
httpRes.error = (error, code, message) => {
|
|
637
|
+
const response = {
|
|
638
|
+
success: false,
|
|
639
|
+
error,
|
|
640
|
+
};
|
|
641
|
+
if (code !== undefined) {
|
|
642
|
+
response.code = code;
|
|
643
|
+
}
|
|
644
|
+
if (message !== undefined) {
|
|
645
|
+
response.message = message;
|
|
646
|
+
}
|
|
647
|
+
httpRes.json(response);
|
|
648
|
+
};
|
|
649
|
+
// Common HTTP error helpers (automatically set status code)
|
|
650
|
+
httpRes.unauthorized = (message = 'Authentication required') => {
|
|
651
|
+
httpRes.statusCode = 401;
|
|
652
|
+
httpRes.json({
|
|
653
|
+
success: false,
|
|
654
|
+
error: 'Unauthorized',
|
|
655
|
+
code: 'UNAUTHORIZED',
|
|
656
|
+
message,
|
|
657
|
+
});
|
|
658
|
+
};
|
|
659
|
+
httpRes.forbidden = (message = 'Insufficient permissions') => {
|
|
660
|
+
httpRes.statusCode = 403;
|
|
661
|
+
httpRes.json({
|
|
662
|
+
success: false,
|
|
663
|
+
error: 'Forbidden',
|
|
664
|
+
code: 'FORBIDDEN',
|
|
665
|
+
message,
|
|
666
|
+
});
|
|
667
|
+
};
|
|
668
|
+
httpRes.notFound = (resource = 'Resource') => {
|
|
669
|
+
httpRes.statusCode = 404;
|
|
670
|
+
httpRes.json({
|
|
671
|
+
success: false,
|
|
672
|
+
error: 'Not Found',
|
|
673
|
+
code: 'NOT_FOUND',
|
|
674
|
+
message: `${resource} not found`,
|
|
675
|
+
});
|
|
676
|
+
};
|
|
677
|
+
httpRes.badRequest = (message = 'Invalid request') => {
|
|
678
|
+
httpRes.statusCode = 400;
|
|
679
|
+
httpRes.json({
|
|
680
|
+
success: false,
|
|
681
|
+
error: 'Bad Request',
|
|
682
|
+
code: 'BAD_REQUEST',
|
|
683
|
+
message,
|
|
684
|
+
});
|
|
685
|
+
};
|
|
686
|
+
httpRes.conflict = (message) => {
|
|
687
|
+
httpRes.statusCode = 409;
|
|
688
|
+
httpRes.json({
|
|
689
|
+
success: false,
|
|
690
|
+
error: 'Conflict',
|
|
691
|
+
code: 'CONFLICT',
|
|
692
|
+
message,
|
|
693
|
+
});
|
|
694
|
+
};
|
|
695
|
+
httpRes.internalError = (message = 'Internal server error') => {
|
|
696
|
+
httpRes.statusCode = 500;
|
|
697
|
+
httpRes.json({
|
|
698
|
+
success: false,
|
|
699
|
+
error: 'Internal Server Error',
|
|
700
|
+
code: 'INTERNAL_ERROR',
|
|
701
|
+
message,
|
|
702
|
+
});
|
|
703
|
+
};
|
|
704
|
+
httpRes.validationError = (errors) => {
|
|
705
|
+
httpRes.statusCode = 422;
|
|
706
|
+
httpRes.json({
|
|
707
|
+
success: false,
|
|
708
|
+
error: 'Validation Failed',
|
|
709
|
+
code: 'VALIDATION_ERROR',
|
|
710
|
+
errors,
|
|
711
|
+
});
|
|
712
|
+
};
|
|
713
|
+
httpRes.rateLimited = (retryAfter) => {
|
|
714
|
+
httpRes.statusCode = 429;
|
|
715
|
+
if (retryAfter) {
|
|
716
|
+
httpRes.setHeader('Retry-After', retryAfter.toString());
|
|
717
|
+
}
|
|
718
|
+
httpRes.json({
|
|
719
|
+
success: false,
|
|
720
|
+
error: 'Rate Limit Exceeded',
|
|
721
|
+
code: 'RATE_LIMITED',
|
|
722
|
+
message: retryAfter
|
|
723
|
+
? `Too many requests. Retry after ${retryAfter} seconds.`
|
|
724
|
+
: 'Too many requests',
|
|
725
|
+
retryAfter,
|
|
726
|
+
});
|
|
727
|
+
};
|
|
728
|
+
// Common success patterns
|
|
729
|
+
httpRes.created = (data, location) => {
|
|
730
|
+
httpRes.statusCode = 201;
|
|
731
|
+
if (location) {
|
|
732
|
+
httpRes.setHeader('Location', location);
|
|
733
|
+
}
|
|
734
|
+
httpRes.json({
|
|
735
|
+
success: true,
|
|
736
|
+
data,
|
|
737
|
+
});
|
|
738
|
+
};
|
|
739
|
+
httpRes.noContent = () => {
|
|
740
|
+
httpRes.statusCode = 204;
|
|
741
|
+
httpRes.end();
|
|
742
|
+
};
|
|
743
|
+
httpRes.paginated = (data, pagination) => {
|
|
744
|
+
const totalPages = Math.ceil(pagination.total / pagination.limit);
|
|
745
|
+
httpRes.json({
|
|
746
|
+
success: true,
|
|
747
|
+
data,
|
|
748
|
+
pagination: {
|
|
749
|
+
page: pagination.page,
|
|
750
|
+
limit: pagination.limit,
|
|
751
|
+
total: pagination.total,
|
|
752
|
+
totalPages,
|
|
753
|
+
hasNext: pagination.page < totalPages,
|
|
754
|
+
hasPrev: pagination.page > 1,
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
};
|
|
684
758
|
// Header management utilities
|
|
685
759
|
httpRes.hasHeader = (name) => {
|
|
686
760
|
return httpRes.getHeader(name) !== undefined;
|
|
@@ -688,15 +762,17 @@ export class MoroHttpServer {
|
|
|
688
762
|
// Note: removeHeader is inherited from ServerResponse, we don't override it
|
|
689
763
|
httpRes.setBulkHeaders = (headers) => {
|
|
690
764
|
if (httpRes.headersSent) {
|
|
765
|
+
// Only enumerate keys for warning if headers were already sent
|
|
766
|
+
const attemptedHeaderKeys = [];
|
|
767
|
+
for (const key in headers) {
|
|
768
|
+
attemptedHeaderKeys.push(key);
|
|
769
|
+
}
|
|
691
770
|
this.logger.warn('Cannot set headers - headers already sent', 'HeaderWarning', {
|
|
692
|
-
attemptedHeaders:
|
|
771
|
+
attemptedHeaders: attemptedHeaderKeys,
|
|
693
772
|
});
|
|
694
773
|
return httpRes;
|
|
695
774
|
}
|
|
696
|
-
const
|
|
697
|
-
const headerKeysLen = headerKeys.length;
|
|
698
|
-
for (let i = 0; i < headerKeysLen; i++) {
|
|
699
|
-
const key = headerKeys[i];
|
|
775
|
+
for (const key in headers) {
|
|
700
776
|
httpRes.setHeader(key, headers[key]);
|
|
701
777
|
}
|
|
702
778
|
return httpRes;
|
|
@@ -765,10 +841,18 @@ export class MoroHttpServer {
|
|
|
765
841
|
return mimeType;
|
|
766
842
|
}
|
|
767
843
|
async parseBody(req) {
|
|
844
|
+
const contentType = req.headers['content-type'] || '';
|
|
845
|
+
const contentLength = parseInt(req.headers['content-length'] || '0');
|
|
846
|
+
const maxSize = 10 * 1024 * 1024; // 10MB limit
|
|
847
|
+
// For very large payloads, return a streaming interface instead of buffering
|
|
848
|
+
if (contentLength > maxSize / 2) {
|
|
849
|
+
// Stream for payloads > 5MB
|
|
850
|
+
return this.createStreamingBodyParser(req, contentType, maxSize);
|
|
851
|
+
}
|
|
852
|
+
// Standard buffered parsing for smaller payloads
|
|
768
853
|
return new Promise((resolve, reject) => {
|
|
769
854
|
const chunks = [];
|
|
770
855
|
let totalLength = 0;
|
|
771
|
-
const maxSize = 10 * 1024 * 1024; // 10MB limit
|
|
772
856
|
req.on('data', (chunk) => {
|
|
773
857
|
totalLength += chunk.length;
|
|
774
858
|
if (totalLength > maxSize) {
|
|
@@ -780,7 +864,6 @@ export class MoroHttpServer {
|
|
|
780
864
|
req.on('end', () => {
|
|
781
865
|
try {
|
|
782
866
|
const body = Buffer.concat(chunks);
|
|
783
|
-
const contentType = req.headers['content-type'] || '';
|
|
784
867
|
if (contentType.includes('application/json')) {
|
|
785
868
|
resolve(JSON.parse(body.toString()));
|
|
786
869
|
}
|
|
@@ -801,6 +884,132 @@ export class MoroHttpServer {
|
|
|
801
884
|
req.on('error', reject);
|
|
802
885
|
});
|
|
803
886
|
}
|
|
887
|
+
/**
|
|
888
|
+
* Create a streaming body parser for large payloads
|
|
889
|
+
* Returns a streaming interface instead of buffering
|
|
890
|
+
*/
|
|
891
|
+
createStreamingBodyParser(req, contentType, maxSize) {
|
|
892
|
+
let totalLength = 0;
|
|
893
|
+
const chunks = [];
|
|
894
|
+
return new Promise((resolve, reject) => {
|
|
895
|
+
const streamParser = {
|
|
896
|
+
// Streaming JSON parser for large JSON payloads
|
|
897
|
+
json: () => this.streamJsonParse(req, maxSize),
|
|
898
|
+
// Streaming form data parser
|
|
899
|
+
form: () => this.streamFormParse(req, maxSize),
|
|
900
|
+
// Raw stream access
|
|
901
|
+
stream: () => ({
|
|
902
|
+
onData: (callback) => {
|
|
903
|
+
req.on('data', (chunk) => {
|
|
904
|
+
totalLength += chunk.length;
|
|
905
|
+
if (totalLength > maxSize) {
|
|
906
|
+
reject(new Error('Request body too large'));
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
909
|
+
callback(chunk);
|
|
910
|
+
});
|
|
911
|
+
},
|
|
912
|
+
onEnd: (callback) => {
|
|
913
|
+
req.on('end', callback);
|
|
914
|
+
},
|
|
915
|
+
onError: (callback) => {
|
|
916
|
+
req.on('error', callback);
|
|
917
|
+
},
|
|
918
|
+
}),
|
|
919
|
+
// Traditional buffered parsing (fallback)
|
|
920
|
+
buffer: async () => {
|
|
921
|
+
return new Promise((resolveBuffer, rejectBuffer) => {
|
|
922
|
+
req.on('data', (chunk) => {
|
|
923
|
+
totalLength += chunk.length;
|
|
924
|
+
if (totalLength > maxSize) {
|
|
925
|
+
rejectBuffer(new Error('Request body too large'));
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
chunks.push(chunk);
|
|
929
|
+
});
|
|
930
|
+
req.on('end', () => {
|
|
931
|
+
try {
|
|
932
|
+
const body = Buffer.concat(chunks);
|
|
933
|
+
if (contentType.includes('application/json')) {
|
|
934
|
+
resolveBuffer(JSON.parse(body.toString()));
|
|
935
|
+
}
|
|
936
|
+
else {
|
|
937
|
+
resolveBuffer(body.toString());
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
catch (error) {
|
|
941
|
+
rejectBuffer(error);
|
|
942
|
+
}
|
|
943
|
+
});
|
|
944
|
+
req.on('error', rejectBuffer);
|
|
945
|
+
});
|
|
946
|
+
},
|
|
947
|
+
};
|
|
948
|
+
// Auto-detect and return appropriate parser
|
|
949
|
+
if (contentType.includes('application/json')) {
|
|
950
|
+
resolve({ type: 'json', parser: streamParser.json });
|
|
951
|
+
}
|
|
952
|
+
else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
953
|
+
resolve({ type: 'form', parser: streamParser.form });
|
|
954
|
+
}
|
|
955
|
+
else {
|
|
956
|
+
resolve({ type: 'stream', parser: streamParser.stream });
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* Streaming JSON parser for large payloads
|
|
962
|
+
*/
|
|
963
|
+
async streamJsonParse(req, maxSize) {
|
|
964
|
+
return new Promise((resolve, reject) => {
|
|
965
|
+
let jsonString = '';
|
|
966
|
+
let totalLength = 0;
|
|
967
|
+
req.on('data', (chunk) => {
|
|
968
|
+
totalLength += chunk.length;
|
|
969
|
+
if (totalLength > maxSize) {
|
|
970
|
+
reject(new Error('Request body too large'));
|
|
971
|
+
return;
|
|
972
|
+
}
|
|
973
|
+
jsonString += chunk.toString();
|
|
974
|
+
});
|
|
975
|
+
req.on('end', () => {
|
|
976
|
+
try {
|
|
977
|
+
// For very large JSON, consider streaming JSON parsing in the future
|
|
978
|
+
resolve(JSON.parse(jsonString));
|
|
979
|
+
}
|
|
980
|
+
catch (error) {
|
|
981
|
+
reject(error);
|
|
982
|
+
}
|
|
983
|
+
});
|
|
984
|
+
req.on('error', reject);
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Streaming form data parser
|
|
989
|
+
*/
|
|
990
|
+
async streamFormParse(req, maxSize) {
|
|
991
|
+
return new Promise((resolve, reject) => {
|
|
992
|
+
let formData = '';
|
|
993
|
+
let totalLength = 0;
|
|
994
|
+
req.on('data', (chunk) => {
|
|
995
|
+
totalLength += chunk.length;
|
|
996
|
+
if (totalLength > maxSize) {
|
|
997
|
+
reject(new Error('Request body too large'));
|
|
998
|
+
return;
|
|
999
|
+
}
|
|
1000
|
+
formData += chunk.toString();
|
|
1001
|
+
});
|
|
1002
|
+
req.on('end', () => {
|
|
1003
|
+
try {
|
|
1004
|
+
resolve(this.parseUrlEncoded(formData));
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
reject(error);
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
1010
|
+
req.on('error', reject);
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
804
1013
|
parseMultipart(buffer, contentType) {
|
|
805
1014
|
const boundary = contentType.split('boundary=')[1];
|
|
806
1015
|
if (!boundary) {
|
|
@@ -898,7 +1107,7 @@ export class MoroHttpServer {
|
|
|
898
1107
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
899
1108
|
return this.routeCache.get(normalizedCacheKey);
|
|
900
1109
|
}
|
|
901
|
-
//
|
|
1110
|
+
// O(1) static route lookup
|
|
902
1111
|
const staticRoute = this.staticRoutes.get(normalizedCacheKey);
|
|
903
1112
|
if (staticRoute) {
|
|
904
1113
|
this.routeCache.set(normalizedCacheKey, staticRoute);
|
|
@@ -907,7 +1116,7 @@ export class MoroHttpServer {
|
|
|
907
1116
|
}
|
|
908
1117
|
return staticRoute;
|
|
909
1118
|
}
|
|
910
|
-
//
|
|
1119
|
+
// Dynamic route matching by segment count
|
|
911
1120
|
let route = null;
|
|
912
1121
|
const dynamicRoutesLen = this.dynamicRoutes.length;
|
|
913
1122
|
if (dynamicRoutesLen > 0) {
|
|
@@ -1011,667 +1220,4 @@ export class MoroHttpServer {
|
|
|
1011
1220
|
};
|
|
1012
1221
|
}
|
|
1013
1222
|
}
|
|
1014
|
-
// Built-in middleware
|
|
1015
|
-
export const middleware = {
|
|
1016
|
-
cors: (options = {}) => {
|
|
1017
|
-
return (req, res, next) => {
|
|
1018
|
-
res.setHeader('Access-Control-Allow-Origin', options.origin || '*');
|
|
1019
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
|
1020
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
1021
|
-
if (options.credentials) {
|
|
1022
|
-
res.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
1023
|
-
}
|
|
1024
|
-
if (req.method === 'OPTIONS') {
|
|
1025
|
-
res.status(200).send('');
|
|
1026
|
-
return;
|
|
1027
|
-
}
|
|
1028
|
-
next();
|
|
1029
|
-
};
|
|
1030
|
-
},
|
|
1031
|
-
helmet: () => {
|
|
1032
|
-
return (req, res, next) => {
|
|
1033
|
-
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
1034
|
-
res.setHeader('X-Frame-Options', 'DENY');
|
|
1035
|
-
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
1036
|
-
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
1037
|
-
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
|
|
1038
|
-
res.setHeader('Content-Security-Policy', "default-src 'self'");
|
|
1039
|
-
next();
|
|
1040
|
-
};
|
|
1041
|
-
},
|
|
1042
|
-
compression: (options = {}) => {
|
|
1043
|
-
const threshold = options.threshold || 1024;
|
|
1044
|
-
const level = options.level || 6;
|
|
1045
|
-
return (req, res, next) => {
|
|
1046
|
-
const acceptEncoding = req.headers['accept-encoding'] || '';
|
|
1047
|
-
// Override res.json to compress responses
|
|
1048
|
-
const originalJson = res.json;
|
|
1049
|
-
const originalSend = res.send;
|
|
1050
|
-
const compressResponse = (data, isJson = false) => {
|
|
1051
|
-
const content = isJson ? JSON.stringify(data) : data;
|
|
1052
|
-
const buffer = Buffer.from(content);
|
|
1053
|
-
if (buffer.length < threshold) {
|
|
1054
|
-
return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
|
|
1055
|
-
}
|
|
1056
|
-
if (acceptEncoding.includes('gzip')) {
|
|
1057
|
-
zlib.gzip(buffer, { level }, (err, compressed) => {
|
|
1058
|
-
if (err) {
|
|
1059
|
-
return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
|
|
1060
|
-
}
|
|
1061
|
-
if (!res.headersSent) {
|
|
1062
|
-
res.setHeader('Content-Encoding', 'gzip');
|
|
1063
|
-
res.setHeader('Content-Length', compressed.length);
|
|
1064
|
-
}
|
|
1065
|
-
res.end(compressed);
|
|
1066
|
-
});
|
|
1067
|
-
}
|
|
1068
|
-
else if (acceptEncoding.includes('deflate')) {
|
|
1069
|
-
zlib.deflate(buffer, { level }, (err, compressed) => {
|
|
1070
|
-
if (err) {
|
|
1071
|
-
return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
|
|
1072
|
-
}
|
|
1073
|
-
if (!res.headersSent) {
|
|
1074
|
-
res.setHeader('Content-Encoding', 'deflate');
|
|
1075
|
-
res.setHeader('Content-Length', compressed.length);
|
|
1076
|
-
}
|
|
1077
|
-
res.end(compressed);
|
|
1078
|
-
});
|
|
1079
|
-
}
|
|
1080
|
-
else {
|
|
1081
|
-
return isJson ? originalJson.call(res, data) : originalSend.call(res, data);
|
|
1082
|
-
}
|
|
1083
|
-
};
|
|
1084
|
-
res.json = function (data) {
|
|
1085
|
-
// Ensure charset is set for Safari compatibility
|
|
1086
|
-
this.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
1087
|
-
compressResponse(data, true);
|
|
1088
|
-
return this;
|
|
1089
|
-
};
|
|
1090
|
-
res.send = function (data) {
|
|
1091
|
-
compressResponse(data, false);
|
|
1092
|
-
return this;
|
|
1093
|
-
};
|
|
1094
|
-
next();
|
|
1095
|
-
};
|
|
1096
|
-
},
|
|
1097
|
-
requestLogger: () => {
|
|
1098
|
-
return (req, res, next) => {
|
|
1099
|
-
const start = Date.now();
|
|
1100
|
-
res.on('finish', () => {
|
|
1101
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1102
|
-
const duration = Date.now() - start;
|
|
1103
|
-
// Request completed - logged by framework
|
|
1104
|
-
});
|
|
1105
|
-
next();
|
|
1106
|
-
};
|
|
1107
|
-
},
|
|
1108
|
-
bodySize: (options = {}) => {
|
|
1109
|
-
const limit = options.limit || '10mb';
|
|
1110
|
-
const limitBytes = parseSize(limit);
|
|
1111
|
-
return (req, res, next) => {
|
|
1112
|
-
const contentLength = parseInt(req.headers['content-length'] || '0');
|
|
1113
|
-
if (contentLength > limitBytes) {
|
|
1114
|
-
res.status(413).json({
|
|
1115
|
-
success: false,
|
|
1116
|
-
error: 'Request entity too large',
|
|
1117
|
-
limit: limit,
|
|
1118
|
-
});
|
|
1119
|
-
return;
|
|
1120
|
-
}
|
|
1121
|
-
next();
|
|
1122
|
-
};
|
|
1123
|
-
},
|
|
1124
|
-
static: (options) => {
|
|
1125
|
-
return async (req, res, next) => {
|
|
1126
|
-
// Only handle GET and HEAD requests
|
|
1127
|
-
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
1128
|
-
next();
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
try {
|
|
1132
|
-
const fs = await import('fs/promises');
|
|
1133
|
-
const path = await import('path');
|
|
1134
|
-
const crypto = await import('crypto');
|
|
1135
|
-
let filePath = path.join(options.root, req.path);
|
|
1136
|
-
// Security: prevent directory traversal
|
|
1137
|
-
if (!filePath.startsWith(path.resolve(options.root))) {
|
|
1138
|
-
res.status(403).json({ success: false, error: 'Forbidden' });
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
// Handle dotfiles
|
|
1142
|
-
const basename = path.basename(filePath);
|
|
1143
|
-
if (basename.startsWith('.')) {
|
|
1144
|
-
if (options.dotfiles === 'deny') {
|
|
1145
|
-
res.status(403).json({ success: false, error: 'Forbidden' });
|
|
1146
|
-
return;
|
|
1147
|
-
}
|
|
1148
|
-
else if (options.dotfiles === 'ignore') {
|
|
1149
|
-
next();
|
|
1150
|
-
return;
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
let stats;
|
|
1154
|
-
try {
|
|
1155
|
-
stats = await fs.stat(filePath);
|
|
1156
|
-
}
|
|
1157
|
-
catch {
|
|
1158
|
-
next(); // File not found, let other middleware handle
|
|
1159
|
-
return;
|
|
1160
|
-
}
|
|
1161
|
-
// Handle directories
|
|
1162
|
-
if (stats.isDirectory()) {
|
|
1163
|
-
const indexFiles = options.index || ['index.html', 'index.htm'];
|
|
1164
|
-
let indexFound = false;
|
|
1165
|
-
for (const indexFile of indexFiles) {
|
|
1166
|
-
const indexPath = path.join(filePath, indexFile);
|
|
1167
|
-
try {
|
|
1168
|
-
const indexStats = await fs.stat(indexPath);
|
|
1169
|
-
if (indexStats.isFile()) {
|
|
1170
|
-
filePath = indexPath;
|
|
1171
|
-
stats = indexStats;
|
|
1172
|
-
indexFound = true;
|
|
1173
|
-
break;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
catch {
|
|
1177
|
-
// Continue to next index file
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
if (!indexFound) {
|
|
1181
|
-
next();
|
|
1182
|
-
return;
|
|
1183
|
-
}
|
|
1184
|
-
}
|
|
1185
|
-
// Set headers with proper mime type and charset
|
|
1186
|
-
const ext = path.extname(filePath);
|
|
1187
|
-
const mimeTypes = {
|
|
1188
|
-
'.html': 'text/html',
|
|
1189
|
-
'.css': 'text/css',
|
|
1190
|
-
'.js': 'application/javascript',
|
|
1191
|
-
'.json': 'application/json',
|
|
1192
|
-
'.png': 'image/png',
|
|
1193
|
-
'.jpg': 'image/jpeg',
|
|
1194
|
-
'.jpeg': 'image/jpeg',
|
|
1195
|
-
'.gif': 'image/gif',
|
|
1196
|
-
'.svg': 'image/svg+xml',
|
|
1197
|
-
'.ico': 'image/x-icon',
|
|
1198
|
-
'.pdf': 'application/pdf',
|
|
1199
|
-
'.txt': 'text/plain',
|
|
1200
|
-
'.xml': 'application/xml',
|
|
1201
|
-
};
|
|
1202
|
-
const baseMimeType = mimeTypes[ext.toLowerCase()] || 'application/octet-stream';
|
|
1203
|
-
// Add charset for text-based files
|
|
1204
|
-
const textTypes = [
|
|
1205
|
-
'text/',
|
|
1206
|
-
'application/json',
|
|
1207
|
-
'application/javascript',
|
|
1208
|
-
'application/xml',
|
|
1209
|
-
'image/svg+xml',
|
|
1210
|
-
];
|
|
1211
|
-
const needsCharset = textTypes.some(type => baseMimeType.startsWith(type));
|
|
1212
|
-
const contentType = needsCharset ? `${baseMimeType}; charset=utf-8` : baseMimeType;
|
|
1213
|
-
res.setHeader('Content-Type', contentType);
|
|
1214
|
-
res.setHeader('Content-Length', stats.size);
|
|
1215
|
-
// Cache headers
|
|
1216
|
-
if (options.maxAge) {
|
|
1217
|
-
res.setHeader('Cache-Control', `public, max-age=${options.maxAge}`);
|
|
1218
|
-
}
|
|
1219
|
-
// ETag support
|
|
1220
|
-
if (options.etag !== false) {
|
|
1221
|
-
const etag = crypto
|
|
1222
|
-
.createHash('md5')
|
|
1223
|
-
.update(`${stats.mtime.getTime()}-${stats.size}`)
|
|
1224
|
-
.digest('hex');
|
|
1225
|
-
res.setHeader('ETag', `"${etag}"`);
|
|
1226
|
-
// Handle conditional requests
|
|
1227
|
-
const ifNoneMatch = req.headers['if-none-match'];
|
|
1228
|
-
if (ifNoneMatch === `"${etag}"`) {
|
|
1229
|
-
res.statusCode = 304;
|
|
1230
|
-
res.end();
|
|
1231
|
-
return;
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
// Handle HEAD requests
|
|
1235
|
-
if (req.method === 'HEAD') {
|
|
1236
|
-
res.end();
|
|
1237
|
-
return;
|
|
1238
|
-
}
|
|
1239
|
-
// Send file
|
|
1240
|
-
const data = await fs.readFile(filePath);
|
|
1241
|
-
res.end(data);
|
|
1242
|
-
}
|
|
1243
|
-
catch {
|
|
1244
|
-
res.status(500).json({ success: false, error: 'Internal server error' });
|
|
1245
|
-
}
|
|
1246
|
-
};
|
|
1247
|
-
},
|
|
1248
|
-
upload: (options = {}) => {
|
|
1249
|
-
return (req, res, next) => {
|
|
1250
|
-
const contentType = req.headers['content-type'] || '';
|
|
1251
|
-
if (!contentType.includes('multipart/form-data')) {
|
|
1252
|
-
next();
|
|
1253
|
-
return;
|
|
1254
|
-
}
|
|
1255
|
-
// File upload handling is now built into parseBody method
|
|
1256
|
-
// This middleware can add additional validation
|
|
1257
|
-
if (req.body && req.body.files) {
|
|
1258
|
-
const files = req.body.files;
|
|
1259
|
-
const maxFileSize = options.maxFileSize || 5 * 1024 * 1024; // 5MB default
|
|
1260
|
-
const maxFiles = options.maxFiles || 10;
|
|
1261
|
-
const allowedTypes = options.allowedTypes;
|
|
1262
|
-
// Validate file count
|
|
1263
|
-
if (Object.keys(files).length > maxFiles) {
|
|
1264
|
-
res.status(400).json({
|
|
1265
|
-
success: false,
|
|
1266
|
-
error: `Too many files. Maximum ${maxFiles} allowed.`,
|
|
1267
|
-
});
|
|
1268
|
-
return;
|
|
1269
|
-
}
|
|
1270
|
-
// Validate each file
|
|
1271
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1272
|
-
for (const [fieldName, file] of Object.entries(files)) {
|
|
1273
|
-
const fileData = file;
|
|
1274
|
-
// Validate file size
|
|
1275
|
-
if (fileData.size > maxFileSize) {
|
|
1276
|
-
res.status(400).json({
|
|
1277
|
-
success: false,
|
|
1278
|
-
error: `File ${fileData.filename} is too large. Maximum ${maxFileSize} bytes allowed.`,
|
|
1279
|
-
});
|
|
1280
|
-
return;
|
|
1281
|
-
}
|
|
1282
|
-
// Validate file type
|
|
1283
|
-
if (allowedTypes && !allowedTypes.includes(fileData.mimetype)) {
|
|
1284
|
-
res.status(400).json({
|
|
1285
|
-
success: false,
|
|
1286
|
-
error: `File type ${fileData.mimetype} not allowed.`,
|
|
1287
|
-
});
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
// Store files in request for easy access
|
|
1292
|
-
req.files = files;
|
|
1293
|
-
}
|
|
1294
|
-
next();
|
|
1295
|
-
};
|
|
1296
|
-
},
|
|
1297
|
-
template: (options) => {
|
|
1298
|
-
const templateCache = new Map();
|
|
1299
|
-
return async (req, res, next) => {
|
|
1300
|
-
// Add render method to response
|
|
1301
|
-
res.render = async (template, data = {}) => {
|
|
1302
|
-
try {
|
|
1303
|
-
const fs = await import('fs/promises');
|
|
1304
|
-
const path = await import('path');
|
|
1305
|
-
const templatePath = path.join(options.views, `${template}.html`);
|
|
1306
|
-
let templateContent;
|
|
1307
|
-
// Check cache first
|
|
1308
|
-
if (options.cache && templateCache.has(templatePath)) {
|
|
1309
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1310
|
-
templateContent = templateCache.get(templatePath);
|
|
1311
|
-
}
|
|
1312
|
-
else {
|
|
1313
|
-
templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
1314
|
-
if (options.cache) {
|
|
1315
|
-
templateCache.set(templatePath, templateContent);
|
|
1316
|
-
}
|
|
1317
|
-
}
|
|
1318
|
-
// Simple template engine - replace {{variable}} with values
|
|
1319
|
-
let rendered = templateContent;
|
|
1320
|
-
// Handle basic variable substitution
|
|
1321
|
-
rendered = rendered.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
1322
|
-
return data[key] !== undefined ? String(data[key]) : match;
|
|
1323
|
-
});
|
|
1324
|
-
// Handle nested object properties like {{user.name}}
|
|
1325
|
-
rendered = rendered.replace(/\{\{([\w.]+)\}\}/g, (match, key) => {
|
|
1326
|
-
const value = key.split('.').reduce((obj, prop) => obj?.[prop], data);
|
|
1327
|
-
return value !== undefined ? String(value) : match;
|
|
1328
|
-
});
|
|
1329
|
-
// Handle loops: {{#each items}}{{name}}{{/each}}
|
|
1330
|
-
rendered = rendered.replace(/\{\{#each (\w+)\}\}(.*?)\{\{\/each\}\}/gs, (match, arrayKey, template) => {
|
|
1331
|
-
const array = data[arrayKey];
|
|
1332
|
-
if (!Array.isArray(array))
|
|
1333
|
-
return '';
|
|
1334
|
-
return array
|
|
1335
|
-
.map(item => {
|
|
1336
|
-
let itemTemplate = template;
|
|
1337
|
-
// Replace variables in the loop template
|
|
1338
|
-
itemTemplate = itemTemplate.replace(/\{\{(\w+)\}\}/g, (match, key) => {
|
|
1339
|
-
return item[key] !== undefined ? String(item[key]) : match;
|
|
1340
|
-
});
|
|
1341
|
-
return itemTemplate;
|
|
1342
|
-
})
|
|
1343
|
-
.join('');
|
|
1344
|
-
});
|
|
1345
|
-
// Handle conditionals: {{#if condition}}content{{/if}}
|
|
1346
|
-
rendered = rendered.replace(/\{\{#if (\w+)\}\}(.*?)\{\{\/if\}\}/gs, (match, conditionKey, content) => {
|
|
1347
|
-
const condition = data[conditionKey];
|
|
1348
|
-
return condition ? content : '';
|
|
1349
|
-
});
|
|
1350
|
-
// Handle layout
|
|
1351
|
-
if (options.defaultLayout) {
|
|
1352
|
-
const layoutPath = path.join(options.views, 'layouts', `${options.defaultLayout}.html`);
|
|
1353
|
-
try {
|
|
1354
|
-
let layoutContent;
|
|
1355
|
-
if (options.cache && templateCache.has(layoutPath)) {
|
|
1356
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1357
|
-
layoutContent = templateCache.get(layoutPath);
|
|
1358
|
-
}
|
|
1359
|
-
else {
|
|
1360
|
-
layoutContent = await fs.readFile(layoutPath, 'utf-8');
|
|
1361
|
-
if (options.cache) {
|
|
1362
|
-
templateCache.set(layoutPath, layoutContent);
|
|
1363
|
-
}
|
|
1364
|
-
}
|
|
1365
|
-
rendered = layoutContent.replace(/\{\{body\}\}/, rendered);
|
|
1366
|
-
}
|
|
1367
|
-
catch {
|
|
1368
|
-
// Layout not found, use template as-is
|
|
1369
|
-
}
|
|
1370
|
-
}
|
|
1371
|
-
res.setHeader('Content-Type', 'text/html');
|
|
1372
|
-
res.end(rendered);
|
|
1373
|
-
}
|
|
1374
|
-
catch {
|
|
1375
|
-
res.status(500).json({ success: false, error: 'Template rendering failed' });
|
|
1376
|
-
}
|
|
1377
|
-
};
|
|
1378
|
-
next();
|
|
1379
|
-
};
|
|
1380
|
-
},
|
|
1381
|
-
// HTTP/2 Server Push middleware
|
|
1382
|
-
http2Push: (options = {}) => {
|
|
1383
|
-
return (req, res, next) => {
|
|
1384
|
-
// Add HTTP/2 push capability to response
|
|
1385
|
-
res.push = (path, options = {}) => {
|
|
1386
|
-
// Check if HTTP/2 is supported
|
|
1387
|
-
if (req.httpVersion === '2.0' && res.stream && res.stream.pushAllowed) {
|
|
1388
|
-
try {
|
|
1389
|
-
const pushStream = res.stream.pushStream({
|
|
1390
|
-
':method': 'GET',
|
|
1391
|
-
':path': path,
|
|
1392
|
-
...options.headers,
|
|
1393
|
-
});
|
|
1394
|
-
if (pushStream) {
|
|
1395
|
-
// Handle push stream
|
|
1396
|
-
return pushStream;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
catch {
|
|
1400
|
-
// Push failed, continue normally
|
|
1401
|
-
}
|
|
1402
|
-
}
|
|
1403
|
-
return null;
|
|
1404
|
-
};
|
|
1405
|
-
// Auto-push configured resources
|
|
1406
|
-
if (options.resources && (!options.condition || options.condition(req))) {
|
|
1407
|
-
for (const resource of options.resources) {
|
|
1408
|
-
res.push?.(resource.path, {
|
|
1409
|
-
headers: {
|
|
1410
|
-
'content-type': resource.type || 'text/plain',
|
|
1411
|
-
},
|
|
1412
|
-
});
|
|
1413
|
-
}
|
|
1414
|
-
}
|
|
1415
|
-
next();
|
|
1416
|
-
};
|
|
1417
|
-
},
|
|
1418
|
-
// Server-Sent Events middleware
|
|
1419
|
-
sse: (options = {}) => {
|
|
1420
|
-
return (req, res, next) => {
|
|
1421
|
-
// Only handle SSE requests
|
|
1422
|
-
if (req.headers.accept?.includes('text/event-stream')) {
|
|
1423
|
-
// Set SSE headers
|
|
1424
|
-
if (!res.headersSent) {
|
|
1425
|
-
res.writeHead(200, {
|
|
1426
|
-
'Content-Type': 'text/event-stream',
|
|
1427
|
-
'Cache-Control': 'no-cache',
|
|
1428
|
-
Connection: 'keep-alive',
|
|
1429
|
-
'Access-Control-Allow-Origin': options.cors ? '*' : undefined,
|
|
1430
|
-
'Access-Control-Allow-Headers': options.cors ? 'Cache-Control' : undefined,
|
|
1431
|
-
});
|
|
1432
|
-
}
|
|
1433
|
-
// Add SSE methods to response
|
|
1434
|
-
res.sendEvent = (data, event, id) => {
|
|
1435
|
-
if (id)
|
|
1436
|
-
res.write(`id: ${id}\n`);
|
|
1437
|
-
if (event)
|
|
1438
|
-
res.write(`event: ${event}\n`);
|
|
1439
|
-
res.write(`data: ${typeof data === 'string' ? data : JSON.stringify(data)}\n\n`);
|
|
1440
|
-
};
|
|
1441
|
-
res.sendComment = (comment) => {
|
|
1442
|
-
res.write(`: ${comment}\n\n`);
|
|
1443
|
-
};
|
|
1444
|
-
res.sendRetry = (ms) => {
|
|
1445
|
-
res.write(`retry: ${ms}\n\n`);
|
|
1446
|
-
};
|
|
1447
|
-
// Set up heartbeat if configured
|
|
1448
|
-
let heartbeatInterval = null;
|
|
1449
|
-
if (options.heartbeat) {
|
|
1450
|
-
heartbeatInterval = setInterval(() => {
|
|
1451
|
-
res.sendComment('heartbeat');
|
|
1452
|
-
}, options.heartbeat);
|
|
1453
|
-
}
|
|
1454
|
-
// Set retry if configured
|
|
1455
|
-
if (options.retry) {
|
|
1456
|
-
res.sendRetry(options.retry);
|
|
1457
|
-
}
|
|
1458
|
-
// Clean up on close
|
|
1459
|
-
req.on('close', () => {
|
|
1460
|
-
if (heartbeatInterval) {
|
|
1461
|
-
clearInterval(heartbeatInterval);
|
|
1462
|
-
}
|
|
1463
|
-
});
|
|
1464
|
-
// Don't call next() - this middleware handles the response
|
|
1465
|
-
return;
|
|
1466
|
-
}
|
|
1467
|
-
next();
|
|
1468
|
-
};
|
|
1469
|
-
},
|
|
1470
|
-
// Range request middleware for streaming
|
|
1471
|
-
range: (options = {}) => {
|
|
1472
|
-
return async (req, res, next) => {
|
|
1473
|
-
// Add range support to response
|
|
1474
|
-
res.sendRange = async (filePath, stats) => {
|
|
1475
|
-
try {
|
|
1476
|
-
const fs = await import('fs/promises');
|
|
1477
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1478
|
-
const _path = await import('path');
|
|
1479
|
-
if (!stats) {
|
|
1480
|
-
stats = await fs.stat(filePath);
|
|
1481
|
-
}
|
|
1482
|
-
const fileSize = stats.size;
|
|
1483
|
-
const range = req.headers.range;
|
|
1484
|
-
// Set Accept-Ranges header
|
|
1485
|
-
res.setHeader('Accept-Ranges', options.acceptRanges || 'bytes');
|
|
1486
|
-
if (!range) {
|
|
1487
|
-
// No range requested, send entire file
|
|
1488
|
-
res.setHeader('Content-Length', fileSize);
|
|
1489
|
-
const data = await fs.readFile(filePath);
|
|
1490
|
-
res.end(data);
|
|
1491
|
-
return;
|
|
1492
|
-
}
|
|
1493
|
-
// Parse range header
|
|
1494
|
-
const ranges = range
|
|
1495
|
-
.replace(/bytes=/, '')
|
|
1496
|
-
.split(',')
|
|
1497
|
-
.map(r => {
|
|
1498
|
-
const [start, end] = r.split('-');
|
|
1499
|
-
return {
|
|
1500
|
-
start: start ? parseInt(start) : 0,
|
|
1501
|
-
end: end ? parseInt(end) : fileSize - 1,
|
|
1502
|
-
};
|
|
1503
|
-
});
|
|
1504
|
-
// Validate ranges
|
|
1505
|
-
if (options.maxRanges && ranges.length > options.maxRanges) {
|
|
1506
|
-
res.status(416).json({ success: false, error: 'Too many ranges' });
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
if (ranges.length === 1) {
|
|
1510
|
-
// Single range
|
|
1511
|
-
const { start, end } = ranges[0];
|
|
1512
|
-
const chunkSize = end - start + 1;
|
|
1513
|
-
if (start >= fileSize || end >= fileSize) {
|
|
1514
|
-
res.status(416);
|
|
1515
|
-
res.setHeader('Content-Range', `bytes */${fileSize}`);
|
|
1516
|
-
res.json({ success: false, error: 'Range not satisfiable' });
|
|
1517
|
-
return;
|
|
1518
|
-
}
|
|
1519
|
-
res.status(206);
|
|
1520
|
-
res.setHeader('Content-Range', `bytes ${start}-${end}/${fileSize}`);
|
|
1521
|
-
res.setHeader('Content-Length', chunkSize);
|
|
1522
|
-
// Stream the range
|
|
1523
|
-
const stream = createReadStream(filePath, {
|
|
1524
|
-
start,
|
|
1525
|
-
end,
|
|
1526
|
-
});
|
|
1527
|
-
stream.pipe(res);
|
|
1528
|
-
}
|
|
1529
|
-
else {
|
|
1530
|
-
// Multiple ranges - multipart response
|
|
1531
|
-
const boundary = 'MULTIPART_BYTERANGES';
|
|
1532
|
-
res.status(206);
|
|
1533
|
-
res.setHeader('Content-Type', `multipart/byteranges; boundary=${boundary}`);
|
|
1534
|
-
for (const { start, end } of ranges) {
|
|
1535
|
-
if (start >= fileSize || end >= fileSize)
|
|
1536
|
-
continue;
|
|
1537
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1538
|
-
const chunkSize = end - start + 1;
|
|
1539
|
-
res.write(`\r\n--${boundary}\r\n`);
|
|
1540
|
-
res.write(`Content-Range: bytes ${start}-${end}/${fileSize}\r\n\r\n`);
|
|
1541
|
-
const stream = createReadStream(filePath, {
|
|
1542
|
-
start,
|
|
1543
|
-
end,
|
|
1544
|
-
});
|
|
1545
|
-
await new Promise(resolve => {
|
|
1546
|
-
stream.on('end', () => resolve());
|
|
1547
|
-
stream.pipe(res, { end: false });
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
res.write(`\r\n--${boundary}--\r\n`);
|
|
1551
|
-
res.end();
|
|
1552
|
-
}
|
|
1553
|
-
}
|
|
1554
|
-
catch {
|
|
1555
|
-
res.status(500).json({ success: false, error: 'Range request failed' });
|
|
1556
|
-
}
|
|
1557
|
-
};
|
|
1558
|
-
next();
|
|
1559
|
-
};
|
|
1560
|
-
},
|
|
1561
|
-
// CSRF Protection middleware
|
|
1562
|
-
csrf: (options = {}) => {
|
|
1563
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1564
|
-
const secret = options.secret || 'moro-csrf-secret';
|
|
1565
|
-
const tokenLength = options.tokenLength || 32;
|
|
1566
|
-
const cookieName = options.cookieName || '_csrf';
|
|
1567
|
-
const headerName = options.headerName || 'x-csrf-token';
|
|
1568
|
-
const ignoreMethods = options.ignoreMethods || ['GET', 'HEAD', 'OPTIONS'];
|
|
1569
|
-
const generateToken = () => {
|
|
1570
|
-
return crypto.randomBytes(tokenLength).toString('hex');
|
|
1571
|
-
};
|
|
1572
|
-
const verifyToken = (token, sessionToken) => {
|
|
1573
|
-
return token && sessionToken && token === sessionToken;
|
|
1574
|
-
};
|
|
1575
|
-
return (req, res, next) => {
|
|
1576
|
-
// Add CSRF token generation method
|
|
1577
|
-
req.csrfToken = () => {
|
|
1578
|
-
if (!req._csrfToken) {
|
|
1579
|
-
req._csrfToken = generateToken();
|
|
1580
|
-
// Set token in cookie
|
|
1581
|
-
res.cookie(cookieName, req._csrfToken, {
|
|
1582
|
-
httpOnly: true,
|
|
1583
|
-
sameSite: options.sameSite !== false ? 'strict' : undefined,
|
|
1584
|
-
secure: req.headers['x-forwarded-proto'] === 'https' || req.socket.encrypted,
|
|
1585
|
-
});
|
|
1586
|
-
}
|
|
1587
|
-
return req._csrfToken;
|
|
1588
|
-
};
|
|
1589
|
-
// Skip verification for safe methods
|
|
1590
|
-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
|
1591
|
-
if (ignoreMethods.includes(req.method)) {
|
|
1592
|
-
next();
|
|
1593
|
-
return;
|
|
1594
|
-
}
|
|
1595
|
-
// Get token from header or body
|
|
1596
|
-
const token = req.headers[headerName] || (req.body && req.body._csrf) || (req.query && req.query._csrf);
|
|
1597
|
-
// Get session token from cookie
|
|
1598
|
-
const sessionToken = req.cookies?.[cookieName];
|
|
1599
|
-
if (!verifyToken(token, sessionToken || '')) {
|
|
1600
|
-
res.status(403).json({
|
|
1601
|
-
success: false,
|
|
1602
|
-
error: 'Invalid CSRF token',
|
|
1603
|
-
code: 'CSRF_TOKEN_MISMATCH',
|
|
1604
|
-
});
|
|
1605
|
-
return;
|
|
1606
|
-
}
|
|
1607
|
-
next();
|
|
1608
|
-
};
|
|
1609
|
-
},
|
|
1610
|
-
// Content Security Policy middleware
|
|
1611
|
-
csp: (options = {}) => {
|
|
1612
|
-
return (req, res, next) => {
|
|
1613
|
-
const directives = options.directives || {
|
|
1614
|
-
defaultSrc: ["'self'"],
|
|
1615
|
-
scriptSrc: ["'self'"],
|
|
1616
|
-
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
1617
|
-
imgSrc: ["'self'", 'data:', 'https:'],
|
|
1618
|
-
connectSrc: ["'self'"],
|
|
1619
|
-
fontSrc: ["'self'"],
|
|
1620
|
-
objectSrc: ["'none'"],
|
|
1621
|
-
mediaSrc: ["'self'"],
|
|
1622
|
-
frameSrc: ["'none'"],
|
|
1623
|
-
};
|
|
1624
|
-
// Generate nonce if requested
|
|
1625
|
-
let nonce;
|
|
1626
|
-
if (options.nonce) {
|
|
1627
|
-
nonce = crypto.randomBytes(16).toString('base64');
|
|
1628
|
-
req.cspNonce = nonce;
|
|
1629
|
-
}
|
|
1630
|
-
// Build CSP header value
|
|
1631
|
-
const cspParts = [];
|
|
1632
|
-
for (const [directive, sources] of Object.entries(directives)) {
|
|
1633
|
-
if (directive === 'upgradeInsecureRequests' && sources === true) {
|
|
1634
|
-
cspParts.push('upgrade-insecure-requests');
|
|
1635
|
-
}
|
|
1636
|
-
else if (directive === 'blockAllMixedContent' && sources === true) {
|
|
1637
|
-
cspParts.push('block-all-mixed-content');
|
|
1638
|
-
}
|
|
1639
|
-
else if (Array.isArray(sources)) {
|
|
1640
|
-
let sourceList = sources.join(' ');
|
|
1641
|
-
// Add nonce to script-src and style-src if enabled
|
|
1642
|
-
if (nonce && (directive === 'scriptSrc' || directive === 'styleSrc')) {
|
|
1643
|
-
sourceList += ` 'nonce-${nonce}'`;
|
|
1644
|
-
}
|
|
1645
|
-
// Convert camelCase to kebab-case
|
|
1646
|
-
const kebabDirective = directive.replace(/([A-Z])/g, '-$1').toLowerCase();
|
|
1647
|
-
cspParts.push(`${kebabDirective} ${sourceList}`);
|
|
1648
|
-
}
|
|
1649
|
-
}
|
|
1650
|
-
// Add report-uri if specified
|
|
1651
|
-
if (options.reportUri) {
|
|
1652
|
-
cspParts.push(`report-uri ${options.reportUri}`);
|
|
1653
|
-
}
|
|
1654
|
-
const cspValue = cspParts.join('; ');
|
|
1655
|
-
const headerName = options.reportOnly
|
|
1656
|
-
? 'Content-Security-Policy-Report-Only'
|
|
1657
|
-
: 'Content-Security-Policy';
|
|
1658
|
-
res.setHeader(headerName, cspValue);
|
|
1659
|
-
next();
|
|
1660
|
-
};
|
|
1661
|
-
},
|
|
1662
|
-
};
|
|
1663
|
-
function parseSize(size) {
|
|
1664
|
-
const units = {
|
|
1665
|
-
b: 1,
|
|
1666
|
-
kb: 1024,
|
|
1667
|
-
mb: 1024 * 1024,
|
|
1668
|
-
gb: 1024 * 1024 * 1024,
|
|
1669
|
-
};
|
|
1670
|
-
const match = size.toLowerCase().match(/^(\d+(?:\.\d+)?)\s*(b|kb|mb|gb)?$/);
|
|
1671
|
-
if (!match)
|
|
1672
|
-
return 1024 * 1024; // Default 1MB
|
|
1673
|
-
const value = parseFloat(match[1]);
|
|
1674
|
-
const unit = match[2] || 'b';
|
|
1675
|
-
return Math.round(value * units[unit]);
|
|
1676
|
-
}
|
|
1677
1223
|
//# sourceMappingURL=http-server.js.map
|