@aegis-fluxion/core 0.7.0 → 0.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 +39 -1
- package/dist/index.cjs +265 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -1
- package/dist/index.d.ts +29 -1
- package/dist/index.js +265 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -15,8 +15,22 @@ interface SecureServerHeartbeatOptions {
|
|
|
15
15
|
intervalMs?: number;
|
|
16
16
|
timeoutMs?: number;
|
|
17
17
|
}
|
|
18
|
+
type SecureServerRateLimitAction = "throttle" | "disconnect";
|
|
19
|
+
interface SecureServerRateLimitOptions {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
windowMs?: number;
|
|
22
|
+
maxEventsPerConnection?: number;
|
|
23
|
+
maxEventsPerIp?: number;
|
|
24
|
+
action?: SecureServerRateLimitAction;
|
|
25
|
+
throttleMs?: number;
|
|
26
|
+
maxThrottleMs?: number;
|
|
27
|
+
disconnectAfterViolations?: number;
|
|
28
|
+
disconnectCode?: number;
|
|
29
|
+
disconnectReason?: string;
|
|
30
|
+
}
|
|
18
31
|
interface SecureServerOptions extends ServerOptions {
|
|
19
32
|
heartbeat?: SecureServerHeartbeatOptions;
|
|
33
|
+
rateLimit?: SecureServerRateLimitOptions;
|
|
20
34
|
}
|
|
21
35
|
interface SecureClientReconnectOptions {
|
|
22
36
|
enabled?: boolean;
|
|
@@ -87,6 +101,7 @@ type SecureServerMiddleware = (context: SecureServerMiddlewareContext, next: Sec
|
|
|
87
101
|
declare class SecureServer {
|
|
88
102
|
private readonly socketServer;
|
|
89
103
|
private readonly heartbeatConfig;
|
|
104
|
+
private readonly rateLimitConfig;
|
|
90
105
|
private heartbeatIntervalHandle;
|
|
91
106
|
private readonly clientsById;
|
|
92
107
|
private readonly clientIdBySocket;
|
|
@@ -105,6 +120,9 @@ declare class SecureServer {
|
|
|
105
120
|
private readonly heartbeatStateBySocket;
|
|
106
121
|
private readonly roomMembersByName;
|
|
107
122
|
private readonly roomNamesByClientId;
|
|
123
|
+
private readonly clientIpByClientId;
|
|
124
|
+
private readonly rateLimitBucketsByClientId;
|
|
125
|
+
private readonly rateLimitBucketsByIp;
|
|
108
126
|
constructor(options: SecureServerOptions);
|
|
109
127
|
get clientCount(): number;
|
|
110
128
|
get clients(): ReadonlyMap<string, SecureServerClient>;
|
|
@@ -127,6 +145,16 @@ declare class SecureServer {
|
|
|
127
145
|
to(room: string): SecureServerRoomOperator;
|
|
128
146
|
close(code?: number, reason?: string): void;
|
|
129
147
|
private resolveHeartbeatConfig;
|
|
148
|
+
private resolveRateLimitConfig;
|
|
149
|
+
private createRateLimitBucket;
|
|
150
|
+
private getOrCreateRateLimitBucket;
|
|
151
|
+
private updateRateLimitBucket;
|
|
152
|
+
private pruneRateLimitBucketMap;
|
|
153
|
+
private pruneRateLimitBuckets;
|
|
154
|
+
private normalizeIpAddress;
|
|
155
|
+
private resolveClientIp;
|
|
156
|
+
private isIpStillConnected;
|
|
157
|
+
private evaluateIncomingRateLimit;
|
|
130
158
|
private startHeartbeatLoop;
|
|
131
159
|
private stopHeartbeatLoop;
|
|
132
160
|
private performHeartbeatSweep;
|
|
@@ -223,4 +251,4 @@ declare class SecureClient {
|
|
|
223
251
|
private flushPendingPayloadQueue;
|
|
224
252
|
}
|
|
225
253
|
|
|
226
|
-
export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
|
|
254
|
+
export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerRateLimitAction, type SecureServerRateLimitOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,8 +15,22 @@ interface SecureServerHeartbeatOptions {
|
|
|
15
15
|
intervalMs?: number;
|
|
16
16
|
timeoutMs?: number;
|
|
17
17
|
}
|
|
18
|
+
type SecureServerRateLimitAction = "throttle" | "disconnect";
|
|
19
|
+
interface SecureServerRateLimitOptions {
|
|
20
|
+
enabled?: boolean;
|
|
21
|
+
windowMs?: number;
|
|
22
|
+
maxEventsPerConnection?: number;
|
|
23
|
+
maxEventsPerIp?: number;
|
|
24
|
+
action?: SecureServerRateLimitAction;
|
|
25
|
+
throttleMs?: number;
|
|
26
|
+
maxThrottleMs?: number;
|
|
27
|
+
disconnectAfterViolations?: number;
|
|
28
|
+
disconnectCode?: number;
|
|
29
|
+
disconnectReason?: string;
|
|
30
|
+
}
|
|
18
31
|
interface SecureServerOptions extends ServerOptions {
|
|
19
32
|
heartbeat?: SecureServerHeartbeatOptions;
|
|
33
|
+
rateLimit?: SecureServerRateLimitOptions;
|
|
20
34
|
}
|
|
21
35
|
interface SecureClientReconnectOptions {
|
|
22
36
|
enabled?: boolean;
|
|
@@ -87,6 +101,7 @@ type SecureServerMiddleware = (context: SecureServerMiddlewareContext, next: Sec
|
|
|
87
101
|
declare class SecureServer {
|
|
88
102
|
private readonly socketServer;
|
|
89
103
|
private readonly heartbeatConfig;
|
|
104
|
+
private readonly rateLimitConfig;
|
|
90
105
|
private heartbeatIntervalHandle;
|
|
91
106
|
private readonly clientsById;
|
|
92
107
|
private readonly clientIdBySocket;
|
|
@@ -105,6 +120,9 @@ declare class SecureServer {
|
|
|
105
120
|
private readonly heartbeatStateBySocket;
|
|
106
121
|
private readonly roomMembersByName;
|
|
107
122
|
private readonly roomNamesByClientId;
|
|
123
|
+
private readonly clientIpByClientId;
|
|
124
|
+
private readonly rateLimitBucketsByClientId;
|
|
125
|
+
private readonly rateLimitBucketsByIp;
|
|
108
126
|
constructor(options: SecureServerOptions);
|
|
109
127
|
get clientCount(): number;
|
|
110
128
|
get clients(): ReadonlyMap<string, SecureServerClient>;
|
|
@@ -127,6 +145,16 @@ declare class SecureServer {
|
|
|
127
145
|
to(room: string): SecureServerRoomOperator;
|
|
128
146
|
close(code?: number, reason?: string): void;
|
|
129
147
|
private resolveHeartbeatConfig;
|
|
148
|
+
private resolveRateLimitConfig;
|
|
149
|
+
private createRateLimitBucket;
|
|
150
|
+
private getOrCreateRateLimitBucket;
|
|
151
|
+
private updateRateLimitBucket;
|
|
152
|
+
private pruneRateLimitBucketMap;
|
|
153
|
+
private pruneRateLimitBuckets;
|
|
154
|
+
private normalizeIpAddress;
|
|
155
|
+
private resolveClientIp;
|
|
156
|
+
private isIpStillConnected;
|
|
157
|
+
private evaluateIncomingRateLimit;
|
|
130
158
|
private startHeartbeatLoop;
|
|
131
159
|
private stopHeartbeatLoop;
|
|
132
160
|
private performHeartbeatSweep;
|
|
@@ -223,4 +251,4 @@ declare class SecureClient {
|
|
|
223
251
|
private flushPendingPayloadQueue;
|
|
224
252
|
}
|
|
225
253
|
|
|
226
|
-
export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
|
|
254
|
+
export { type SecureAckCallback, type SecureAckOptions, type SecureBinaryPayload, SecureClient, type SecureClientConnectHandler, type SecureClientDisconnectHandler, type SecureClientEventHandler, type SecureClientEventMap, type SecureClientLifecycleEvent, type SecureClientOptions, type SecureClientReadyHandler, type SecureClientReconnectOptions, type SecureEnvelope, type SecureErrorHandler, SecureServer, type SecureServerClient, type SecureServerConnectionHandler, type SecureServerConnectionMiddlewareContext, type SecureServerDisconnectHandler, type SecureServerEventHandler, type SecureServerEventMap, type SecureServerHeartbeatOptions, type SecureServerLifecycleEvent, type SecureServerMessageMiddlewareContext, type SecureServerMiddleware, type SecureServerMiddlewareContext, type SecureServerMiddlewareNext, type SecureServerOptions, type SecureServerRateLimitAction, type SecureServerRateLimitOptions, type SecureServerReadyHandler, type SecureServerRoomOperator };
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,14 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
|
26
26
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
27
27
|
var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
|
|
28
28
|
var DEFAULT_RPC_TIMEOUT_MS = 5e3;
|
|
29
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS = 1e3;
|
|
30
|
+
var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION = 120;
|
|
31
|
+
var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP = 300;
|
|
32
|
+
var DEFAULT_RATE_LIMIT_THROTTLE_MS = 150;
|
|
33
|
+
var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
|
|
34
|
+
var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
|
|
35
|
+
var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
|
|
36
|
+
var DEFAULT_RATE_LIMIT_CLOSE_REASON = "Rate limit exceeded. Please retry later.";
|
|
29
37
|
function normalizeToError(error, fallbackMessage) {
|
|
30
38
|
if (error instanceof Error) {
|
|
31
39
|
return error;
|
|
@@ -59,6 +67,11 @@ function rawDataToBuffer(rawData) {
|
|
|
59
67
|
}
|
|
60
68
|
return Buffer.from(rawData);
|
|
61
69
|
}
|
|
70
|
+
function delay(ms) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
setTimeout(resolve, ms);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
62
75
|
function isBlobValue(value) {
|
|
63
76
|
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
64
77
|
}
|
|
@@ -351,6 +364,7 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
|
|
|
351
364
|
var SecureServer = class {
|
|
352
365
|
socketServer;
|
|
353
366
|
heartbeatConfig;
|
|
367
|
+
rateLimitConfig;
|
|
354
368
|
heartbeatIntervalHandle = null;
|
|
355
369
|
clientsById = /* @__PURE__ */ new Map();
|
|
356
370
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
@@ -369,9 +383,13 @@ var SecureServer = class {
|
|
|
369
383
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
370
384
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
371
385
|
roomNamesByClientId = /* @__PURE__ */ new Map();
|
|
386
|
+
clientIpByClientId = /* @__PURE__ */ new Map();
|
|
387
|
+
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
388
|
+
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
372
389
|
constructor(options) {
|
|
373
|
-
const { heartbeat, ...socketServerOptions } = options;
|
|
390
|
+
const { heartbeat, rateLimit, ...socketServerOptions } = options;
|
|
374
391
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
392
|
+
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
375
393
|
this.socketServer = new WebSocketServer(socketServerOptions);
|
|
376
394
|
this.bindSocketServerEvents();
|
|
377
395
|
this.startHeartbeatLoop();
|
|
@@ -553,6 +571,9 @@ var SecureServer = class {
|
|
|
553
571
|
client.socket.close(code, reason);
|
|
554
572
|
}
|
|
555
573
|
}
|
|
574
|
+
this.rateLimitBucketsByClientId.clear();
|
|
575
|
+
this.rateLimitBucketsByIp.clear();
|
|
576
|
+
this.clientIpByClientId.clear();
|
|
556
577
|
this.socketServer.close();
|
|
557
578
|
} catch (error) {
|
|
558
579
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
@@ -573,6 +594,211 @@ var SecureServer = class {
|
|
|
573
594
|
timeoutMs
|
|
574
595
|
};
|
|
575
596
|
}
|
|
597
|
+
resolveRateLimitConfig(rateLimitOptions) {
|
|
598
|
+
const windowMs = rateLimitOptions?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
599
|
+
const maxEventsPerConnection = rateLimitOptions?.maxEventsPerConnection ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION;
|
|
600
|
+
const maxEventsPerIp = rateLimitOptions?.maxEventsPerIp ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP;
|
|
601
|
+
const action = rateLimitOptions?.action ?? "throttle";
|
|
602
|
+
const throttleMs = rateLimitOptions?.throttleMs ?? DEFAULT_RATE_LIMIT_THROTTLE_MS;
|
|
603
|
+
const maxThrottleMs = rateLimitOptions?.maxThrottleMs ?? DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS;
|
|
604
|
+
const disconnectAfterViolations = rateLimitOptions?.disconnectAfterViolations ?? DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS;
|
|
605
|
+
const disconnectCode = rateLimitOptions?.disconnectCode ?? DEFAULT_RATE_LIMIT_CLOSE_CODE;
|
|
606
|
+
const disconnectReason = rateLimitOptions?.disconnectReason ?? DEFAULT_RATE_LIMIT_CLOSE_REASON;
|
|
607
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
608
|
+
throw new Error("Server rateLimit windowMs must be a positive number.");
|
|
609
|
+
}
|
|
610
|
+
if (!Number.isFinite(maxEventsPerConnection) || maxEventsPerConnection <= 0) {
|
|
611
|
+
throw new Error(
|
|
612
|
+
"Server rateLimit maxEventsPerConnection must be a positive number."
|
|
613
|
+
);
|
|
614
|
+
}
|
|
615
|
+
if (!Number.isFinite(maxEventsPerIp) || maxEventsPerIp <= 0) {
|
|
616
|
+
throw new Error("Server rateLimit maxEventsPerIp must be a positive number.");
|
|
617
|
+
}
|
|
618
|
+
if (action !== "throttle" && action !== "disconnect") {
|
|
619
|
+
throw new Error('Server rateLimit action must be either "throttle" or "disconnect".');
|
|
620
|
+
}
|
|
621
|
+
if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
|
|
622
|
+
throw new Error("Server rateLimit throttleMs must be a positive number.");
|
|
623
|
+
}
|
|
624
|
+
if (!Number.isFinite(maxThrottleMs) || maxThrottleMs <= 0) {
|
|
625
|
+
throw new Error("Server rateLimit maxThrottleMs must be a positive number.");
|
|
626
|
+
}
|
|
627
|
+
if (maxThrottleMs < throttleMs) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"Server rateLimit maxThrottleMs must be greater than or equal to throttleMs."
|
|
630
|
+
);
|
|
631
|
+
}
|
|
632
|
+
if (!Number.isInteger(disconnectAfterViolations) || disconnectAfterViolations <= 0) {
|
|
633
|
+
throw new Error(
|
|
634
|
+
"Server rateLimit disconnectAfterViolations must be a positive integer."
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
if (!Number.isInteger(disconnectCode) || disconnectCode < 1e3 || disconnectCode > 4999) {
|
|
638
|
+
throw new Error("Server rateLimit disconnectCode must be a valid WebSocket close code.");
|
|
639
|
+
}
|
|
640
|
+
return {
|
|
641
|
+
enabled: rateLimitOptions?.enabled ?? true,
|
|
642
|
+
windowMs,
|
|
643
|
+
maxEventsPerConnection,
|
|
644
|
+
maxEventsPerIp,
|
|
645
|
+
action,
|
|
646
|
+
throttleMs,
|
|
647
|
+
maxThrottleMs,
|
|
648
|
+
disconnectAfterViolations,
|
|
649
|
+
disconnectCode,
|
|
650
|
+
disconnectReason
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
createRateLimitBucket(now) {
|
|
654
|
+
return {
|
|
655
|
+
windowStartedAt: now,
|
|
656
|
+
count: 0,
|
|
657
|
+
violationCount: 0,
|
|
658
|
+
throttleUntil: 0,
|
|
659
|
+
lastSeenAt: now
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
getOrCreateRateLimitBucket(map, key, now) {
|
|
663
|
+
const existingBucket = map.get(key);
|
|
664
|
+
if (existingBucket) {
|
|
665
|
+
return existingBucket;
|
|
666
|
+
}
|
|
667
|
+
const bucket = this.createRateLimitBucket(now);
|
|
668
|
+
map.set(key, bucket);
|
|
669
|
+
return bucket;
|
|
670
|
+
}
|
|
671
|
+
updateRateLimitBucket(bucket, now) {
|
|
672
|
+
if (now - bucket.windowStartedAt >= this.rateLimitConfig.windowMs) {
|
|
673
|
+
bucket.windowStartedAt = now;
|
|
674
|
+
bucket.count = 0;
|
|
675
|
+
bucket.violationCount = 0;
|
|
676
|
+
bucket.throttleUntil = 0;
|
|
677
|
+
}
|
|
678
|
+
bucket.count += 1;
|
|
679
|
+
bucket.lastSeenAt = now;
|
|
680
|
+
}
|
|
681
|
+
pruneRateLimitBucketMap(map, now, maxIdleMs) {
|
|
682
|
+
for (const [key, bucket] of map.entries()) {
|
|
683
|
+
if (now - bucket.lastSeenAt >= maxIdleMs) {
|
|
684
|
+
map.delete(key);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
pruneRateLimitBuckets(now) {
|
|
689
|
+
const maxIdleMs = this.rateLimitConfig.windowMs * 4;
|
|
690
|
+
this.pruneRateLimitBucketMap(this.rateLimitBucketsByClientId, now, maxIdleMs);
|
|
691
|
+
this.pruneRateLimitBucketMap(this.rateLimitBucketsByIp, now, maxIdleMs);
|
|
692
|
+
}
|
|
693
|
+
normalizeIpAddress(ipAddress) {
|
|
694
|
+
let normalized = ipAddress.trim().toLowerCase();
|
|
695
|
+
if (normalized.startsWith("::ffff:")) {
|
|
696
|
+
normalized = normalized.slice(7);
|
|
697
|
+
}
|
|
698
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
|
699
|
+
normalized = normalized.slice(1, -1);
|
|
700
|
+
}
|
|
701
|
+
const zoneIndex = normalized.indexOf("%");
|
|
702
|
+
if (zoneIndex >= 0) {
|
|
703
|
+
normalized = normalized.slice(0, zoneIndex);
|
|
704
|
+
}
|
|
705
|
+
return normalized.length > 0 ? normalized : "unknown";
|
|
706
|
+
}
|
|
707
|
+
resolveClientIp(request) {
|
|
708
|
+
const forwardedHeader = request.headers["x-forwarded-for"];
|
|
709
|
+
const forwardedValue = Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader;
|
|
710
|
+
if (typeof forwardedValue === "string") {
|
|
711
|
+
const firstForwardedIp = forwardedValue.split(",").map((item) => item.trim()).find((item) => item.length > 0);
|
|
712
|
+
if (firstForwardedIp) {
|
|
713
|
+
return this.normalizeIpAddress(firstForwardedIp);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return this.normalizeIpAddress(request.socket.remoteAddress ?? "unknown");
|
|
717
|
+
}
|
|
718
|
+
isIpStillConnected(ipAddress) {
|
|
719
|
+
for (const connectedIp of this.clientIpByClientId.values()) {
|
|
720
|
+
if (connectedIp === ipAddress) {
|
|
721
|
+
return true;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
evaluateIncomingRateLimit(client) {
|
|
727
|
+
const noLimitDecision = {
|
|
728
|
+
shouldDisconnect: false,
|
|
729
|
+
shouldDrop: false,
|
|
730
|
+
throttleDelayMs: 0
|
|
731
|
+
};
|
|
732
|
+
if (!this.rateLimitConfig.enabled) {
|
|
733
|
+
return noLimitDecision;
|
|
734
|
+
}
|
|
735
|
+
const now = Date.now();
|
|
736
|
+
const clientBucket = this.getOrCreateRateLimitBucket(
|
|
737
|
+
this.rateLimitBucketsByClientId,
|
|
738
|
+
client.id,
|
|
739
|
+
now
|
|
740
|
+
);
|
|
741
|
+
this.updateRateLimitBucket(clientBucket, now);
|
|
742
|
+
const clientIp = this.clientIpByClientId.get(client.id);
|
|
743
|
+
const ipBucket = clientIp ? this.getOrCreateRateLimitBucket(this.rateLimitBucketsByIp, clientIp, now) : null;
|
|
744
|
+
if (ipBucket) {
|
|
745
|
+
this.updateRateLimitBucket(ipBucket, now);
|
|
746
|
+
}
|
|
747
|
+
const activeThrottleUntil = Math.max(
|
|
748
|
+
clientBucket.throttleUntil,
|
|
749
|
+
ipBucket?.throttleUntil ?? 0
|
|
750
|
+
);
|
|
751
|
+
if (activeThrottleUntil > now) {
|
|
752
|
+
return {
|
|
753
|
+
shouldDisconnect: false,
|
|
754
|
+
shouldDrop: true,
|
|
755
|
+
throttleDelayMs: 0
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
const isConnectionLimitExceeded = clientBucket.count > this.rateLimitConfig.maxEventsPerConnection;
|
|
759
|
+
const isIpLimitExceeded = ipBucket ? ipBucket.count > this.rateLimitConfig.maxEventsPerIp : false;
|
|
760
|
+
if (!isConnectionLimitExceeded && !isIpLimitExceeded) {
|
|
761
|
+
if (this.rateLimitBucketsByClientId.size > 1024 || this.rateLimitBucketsByIp.size > 1024) {
|
|
762
|
+
this.pruneRateLimitBuckets(now);
|
|
763
|
+
}
|
|
764
|
+
return noLimitDecision;
|
|
765
|
+
}
|
|
766
|
+
if (isConnectionLimitExceeded) {
|
|
767
|
+
clientBucket.violationCount += 1;
|
|
768
|
+
}
|
|
769
|
+
if (ipBucket && isIpLimitExceeded) {
|
|
770
|
+
ipBucket.violationCount += 1;
|
|
771
|
+
}
|
|
772
|
+
const violationCount = Math.max(
|
|
773
|
+
clientBucket.violationCount,
|
|
774
|
+
ipBucket?.violationCount ?? 0
|
|
775
|
+
);
|
|
776
|
+
const shouldDisconnect = this.rateLimitConfig.action === "disconnect" || violationCount >= this.rateLimitConfig.disconnectAfterViolations;
|
|
777
|
+
if (shouldDisconnect) {
|
|
778
|
+
return {
|
|
779
|
+
shouldDisconnect: true,
|
|
780
|
+
shouldDrop: true,
|
|
781
|
+
throttleDelayMs: 0
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
const throttleDelayMs = Math.min(
|
|
785
|
+
this.rateLimitConfig.maxThrottleMs,
|
|
786
|
+
Math.max(
|
|
787
|
+
this.rateLimitConfig.throttleMs,
|
|
788
|
+
this.rateLimitConfig.throttleMs * violationCount
|
|
789
|
+
)
|
|
790
|
+
);
|
|
791
|
+
const throttleUntil = now + throttleDelayMs;
|
|
792
|
+
clientBucket.throttleUntil = throttleUntil;
|
|
793
|
+
if (ipBucket) {
|
|
794
|
+
ipBucket.throttleUntil = throttleUntil;
|
|
795
|
+
}
|
|
796
|
+
return {
|
|
797
|
+
shouldDisconnect: false,
|
|
798
|
+
shouldDrop: false,
|
|
799
|
+
throttleDelayMs
|
|
800
|
+
};
|
|
801
|
+
}
|
|
576
802
|
startHeartbeatLoop() {
|
|
577
803
|
if (!this.heartbeatConfig.enabled || this.heartbeatIntervalHandle) {
|
|
578
804
|
return;
|
|
@@ -675,6 +901,8 @@ var SecureServer = class {
|
|
|
675
901
|
try {
|
|
676
902
|
const clientId = randomUUID();
|
|
677
903
|
const handshakeState = this.createServerHandshakeState();
|
|
904
|
+
const clientIp = this.resolveClientIp(request);
|
|
905
|
+
connectionMetadata.set("network.ip", clientIp);
|
|
678
906
|
const client = this.createSecureServerClient(
|
|
679
907
|
clientId,
|
|
680
908
|
socket,
|
|
@@ -683,6 +911,7 @@ var SecureServer = class {
|
|
|
683
911
|
);
|
|
684
912
|
this.clientsById.set(clientId, client);
|
|
685
913
|
this.clientIdBySocket.set(socket, clientId);
|
|
914
|
+
this.clientIpByClientId.set(clientId, clientIp);
|
|
686
915
|
this.handshakeStateBySocket.set(socket, handshakeState);
|
|
687
916
|
this.pendingPayloadsBySocket.set(socket, []);
|
|
688
917
|
this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
|
|
@@ -717,6 +946,35 @@ var SecureServer = class {
|
|
|
717
946
|
}
|
|
718
947
|
async handleIncomingMessage(client, rawData) {
|
|
719
948
|
try {
|
|
949
|
+
const rateLimitDecision = this.evaluateIncomingRateLimit(client);
|
|
950
|
+
if (rateLimitDecision.shouldDisconnect) {
|
|
951
|
+
this.notifyError(
|
|
952
|
+
new Error(
|
|
953
|
+
`Rate limit disconnect triggered for client ${client.id}.`
|
|
954
|
+
)
|
|
955
|
+
);
|
|
956
|
+
if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
|
|
957
|
+
client.socket.close(
|
|
958
|
+
this.rateLimitConfig.disconnectCode,
|
|
959
|
+
this.rateLimitConfig.disconnectReason
|
|
960
|
+
);
|
|
961
|
+
}
|
|
962
|
+
return;
|
|
963
|
+
}
|
|
964
|
+
if (rateLimitDecision.shouldDrop) {
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (rateLimitDecision.throttleDelayMs > 0) {
|
|
968
|
+
this.notifyError(
|
|
969
|
+
new Error(
|
|
970
|
+
`Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
|
|
971
|
+
)
|
|
972
|
+
);
|
|
973
|
+
await delay(rateLimitDecision.throttleDelayMs);
|
|
974
|
+
if (client.socket.readyState !== WebSocket.OPEN) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
720
978
|
let envelope = null;
|
|
721
979
|
try {
|
|
722
980
|
envelope = parseEnvelope(rawData);
|
|
@@ -778,6 +1036,12 @@ var SecureServer = class {
|
|
|
778
1036
|
client.leaveAll();
|
|
779
1037
|
this.clientsById.delete(client.id);
|
|
780
1038
|
this.clientIdBySocket.delete(client.socket);
|
|
1039
|
+
const disconnectedIp = this.clientIpByClientId.get(client.id);
|
|
1040
|
+
this.clientIpByClientId.delete(client.id);
|
|
1041
|
+
this.rateLimitBucketsByClientId.delete(client.id);
|
|
1042
|
+
if (disconnectedIp && !this.isIpStillConnected(disconnectedIp)) {
|
|
1043
|
+
this.rateLimitBucketsByIp.delete(disconnectedIp);
|
|
1044
|
+
}
|
|
781
1045
|
this.handshakeStateBySocket.delete(client.socket);
|
|
782
1046
|
this.sharedSecretBySocket.delete(client.socket);
|
|
783
1047
|
this.encryptionKeyBySocket.delete(client.socket);
|