@aegis-fluxion/core 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -217
- package/dist/index.cjs +446 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -1
- package/dist/index.d.ts +54 -1
- package/dist/index.js +446 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -26,6 +26,59 @@ 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.";
|
|
37
|
+
var SECURE_SERVER_ADAPTER_MESSAGE_VERSION = 1;
|
|
38
|
+
function normalizeSecureServerAdapterMessage(value) {
|
|
39
|
+
if (!isPlainObject(value)) {
|
|
40
|
+
throw new Error("SecureServer adapter message must be a plain object.");
|
|
41
|
+
}
|
|
42
|
+
if (value.version !== SECURE_SERVER_ADAPTER_MESSAGE_VERSION) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`Unsupported SecureServer adapter message version: ${String(value.version)}.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (typeof value.originServerId !== "string" || value.originServerId.trim().length === 0) {
|
|
48
|
+
throw new Error("SecureServer adapter message originServerId must be a non-empty string.");
|
|
49
|
+
}
|
|
50
|
+
if (value.scope !== "broadcast" && value.scope !== "room") {
|
|
51
|
+
throw new Error('SecureServer adapter message scope must be either "broadcast" or "room".');
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.event !== "string" || value.event.trim().length === 0) {
|
|
54
|
+
throw new Error("SecureServer adapter message event must be a non-empty string.");
|
|
55
|
+
}
|
|
56
|
+
if (typeof value.emittedAt !== "number" || !Number.isFinite(value.emittedAt)) {
|
|
57
|
+
throw new Error("SecureServer adapter message emittedAt must be a finite number.");
|
|
58
|
+
}
|
|
59
|
+
if (value.scope === "room") {
|
|
60
|
+
if (typeof value.room !== "string" || value.room.trim().length === 0) {
|
|
61
|
+
throw new Error("SecureServer adapter message room must be a non-empty string.");
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
65
|
+
originServerId: value.originServerId,
|
|
66
|
+
scope: value.scope,
|
|
67
|
+
event: value.event,
|
|
68
|
+
data: value.data,
|
|
69
|
+
emittedAt: value.emittedAt,
|
|
70
|
+
room: value.room.trim()
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
75
|
+
originServerId: value.originServerId,
|
|
76
|
+
scope: value.scope,
|
|
77
|
+
event: value.event,
|
|
78
|
+
data: value.data,
|
|
79
|
+
emittedAt: value.emittedAt
|
|
80
|
+
};
|
|
81
|
+
}
|
|
29
82
|
function normalizeToError(error, fallbackMessage) {
|
|
30
83
|
if (error instanceof Error) {
|
|
31
84
|
return error;
|
|
@@ -59,6 +112,11 @@ function rawDataToBuffer(rawData) {
|
|
|
59
112
|
}
|
|
60
113
|
return Buffer.from(rawData);
|
|
61
114
|
}
|
|
115
|
+
function delay(ms) {
|
|
116
|
+
return new Promise((resolve) => {
|
|
117
|
+
setTimeout(resolve, ms);
|
|
118
|
+
});
|
|
119
|
+
}
|
|
62
120
|
function isBlobValue(value) {
|
|
63
121
|
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
64
122
|
}
|
|
@@ -349,8 +407,11 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
|
|
|
349
407
|
return plaintext.toString("utf8");
|
|
350
408
|
}
|
|
351
409
|
var SecureServer = class {
|
|
410
|
+
instanceId = randomUUID();
|
|
352
411
|
socketServer;
|
|
412
|
+
adapter = null;
|
|
353
413
|
heartbeatConfig;
|
|
414
|
+
rateLimitConfig;
|
|
354
415
|
heartbeatIntervalHandle = null;
|
|
355
416
|
clientsById = /* @__PURE__ */ new Map();
|
|
356
417
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
@@ -369,19 +430,80 @@ var SecureServer = class {
|
|
|
369
430
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
370
431
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
371
432
|
roomNamesByClientId = /* @__PURE__ */ new Map();
|
|
433
|
+
clientIpByClientId = /* @__PURE__ */ new Map();
|
|
434
|
+
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
435
|
+
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
372
436
|
constructor(options) {
|
|
373
|
-
const { heartbeat, ...socketServerOptions } = options;
|
|
437
|
+
const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
|
|
374
438
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
439
|
+
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
375
440
|
this.socketServer = new WebSocketServer(socketServerOptions);
|
|
376
441
|
this.bindSocketServerEvents();
|
|
377
442
|
this.startHeartbeatLoop();
|
|
443
|
+
if (adapter) {
|
|
444
|
+
void this.setAdapter(adapter).catch(() => {
|
|
445
|
+
return void 0;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
378
448
|
}
|
|
379
449
|
get clientCount() {
|
|
380
450
|
return this.clientsById.size;
|
|
381
451
|
}
|
|
452
|
+
get serverId() {
|
|
453
|
+
return this.instanceId;
|
|
454
|
+
}
|
|
382
455
|
get clients() {
|
|
383
456
|
return this.clientsById;
|
|
384
457
|
}
|
|
458
|
+
async setAdapter(adapter) {
|
|
459
|
+
const previousAdapter = this.adapter;
|
|
460
|
+
if (previousAdapter === adapter) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
try {
|
|
464
|
+
if (previousAdapter?.detach) {
|
|
465
|
+
await Promise.resolve(previousAdapter.detach(this));
|
|
466
|
+
}
|
|
467
|
+
this.adapter = null;
|
|
468
|
+
if (!adapter) {
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
await Promise.resolve(adapter.attach(this));
|
|
472
|
+
this.adapter = adapter;
|
|
473
|
+
} catch (error) {
|
|
474
|
+
const normalizedError = normalizeToError(
|
|
475
|
+
error,
|
|
476
|
+
"Failed to set SecureServer adapter."
|
|
477
|
+
);
|
|
478
|
+
this.notifyError(normalizedError);
|
|
479
|
+
throw normalizedError;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async handleAdapterMessage(message) {
|
|
483
|
+
try {
|
|
484
|
+
const normalizedMessage = normalizeSecureServerAdapterMessage(message);
|
|
485
|
+
if (normalizedMessage.originServerId === this.instanceId) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (normalizedMessage.scope === "broadcast") {
|
|
489
|
+
this.emitLocally(normalizedMessage.event, normalizedMessage.data);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (!normalizedMessage.room) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
this.emitToRoom(
|
|
496
|
+
normalizedMessage.room,
|
|
497
|
+
normalizedMessage.event,
|
|
498
|
+
normalizedMessage.data,
|
|
499
|
+
false
|
|
500
|
+
);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this.notifyError(
|
|
503
|
+
normalizeToError(error, "Failed to process SecureServer adapter message.")
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
385
507
|
on(event, handler) {
|
|
386
508
|
try {
|
|
387
509
|
if (event === "connection") {
|
|
@@ -468,12 +590,12 @@ var SecureServer = class {
|
|
|
468
590
|
if (isReservedEmitEvent(event)) {
|
|
469
591
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
470
592
|
}
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
593
|
+
this.emitLocally(event, data);
|
|
594
|
+
this.publishAdapterMessage({
|
|
595
|
+
scope: "broadcast",
|
|
596
|
+
event,
|
|
597
|
+
data
|
|
598
|
+
});
|
|
477
599
|
} catch (error) {
|
|
478
600
|
this.notifyError(normalizeToError(error, "Failed to emit server event."));
|
|
479
601
|
}
|
|
@@ -530,7 +652,7 @@ var SecureServer = class {
|
|
|
530
652
|
return {
|
|
531
653
|
emit: (event, data) => {
|
|
532
654
|
try {
|
|
533
|
-
this.emitToRoom(normalizedRoom, event, data);
|
|
655
|
+
this.emitToRoom(normalizedRoom, event, data, true);
|
|
534
656
|
} catch (error) {
|
|
535
657
|
this.notifyError(
|
|
536
658
|
normalizeToError(error, `Failed to emit event to room ${normalizedRoom}.`)
|
|
@@ -543,6 +665,15 @@ var SecureServer = class {
|
|
|
543
665
|
close(code = DEFAULT_CLOSE_CODE, reason = DEFAULT_CLOSE_REASON) {
|
|
544
666
|
try {
|
|
545
667
|
this.stopHeartbeatLoop();
|
|
668
|
+
const activeAdapter = this.adapter;
|
|
669
|
+
this.adapter = null;
|
|
670
|
+
if (activeAdapter?.detach) {
|
|
671
|
+
void Promise.resolve(activeAdapter.detach(this)).catch((error) => {
|
|
672
|
+
this.notifyError(
|
|
673
|
+
normalizeToError(error, "Failed to detach SecureServer adapter during close.")
|
|
674
|
+
);
|
|
675
|
+
});
|
|
676
|
+
}
|
|
546
677
|
for (const client of this.clientsById.values()) {
|
|
547
678
|
this.rejectPendingRpcRequests(
|
|
548
679
|
client.socket,
|
|
@@ -553,6 +684,9 @@ var SecureServer = class {
|
|
|
553
684
|
client.socket.close(code, reason);
|
|
554
685
|
}
|
|
555
686
|
}
|
|
687
|
+
this.rateLimitBucketsByClientId.clear();
|
|
688
|
+
this.rateLimitBucketsByIp.clear();
|
|
689
|
+
this.clientIpByClientId.clear();
|
|
556
690
|
this.socketServer.close();
|
|
557
691
|
} catch (error) {
|
|
558
692
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
@@ -573,6 +707,211 @@ var SecureServer = class {
|
|
|
573
707
|
timeoutMs
|
|
574
708
|
};
|
|
575
709
|
}
|
|
710
|
+
resolveRateLimitConfig(rateLimitOptions) {
|
|
711
|
+
const windowMs = rateLimitOptions?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
712
|
+
const maxEventsPerConnection = rateLimitOptions?.maxEventsPerConnection ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION;
|
|
713
|
+
const maxEventsPerIp = rateLimitOptions?.maxEventsPerIp ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP;
|
|
714
|
+
const action = rateLimitOptions?.action ?? "throttle";
|
|
715
|
+
const throttleMs = rateLimitOptions?.throttleMs ?? DEFAULT_RATE_LIMIT_THROTTLE_MS;
|
|
716
|
+
const maxThrottleMs = rateLimitOptions?.maxThrottleMs ?? DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS;
|
|
717
|
+
const disconnectAfterViolations = rateLimitOptions?.disconnectAfterViolations ?? DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS;
|
|
718
|
+
const disconnectCode = rateLimitOptions?.disconnectCode ?? DEFAULT_RATE_LIMIT_CLOSE_CODE;
|
|
719
|
+
const disconnectReason = rateLimitOptions?.disconnectReason ?? DEFAULT_RATE_LIMIT_CLOSE_REASON;
|
|
720
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
721
|
+
throw new Error("Server rateLimit windowMs must be a positive number.");
|
|
722
|
+
}
|
|
723
|
+
if (!Number.isFinite(maxEventsPerConnection) || maxEventsPerConnection <= 0) {
|
|
724
|
+
throw new Error(
|
|
725
|
+
"Server rateLimit maxEventsPerConnection must be a positive number."
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
if (!Number.isFinite(maxEventsPerIp) || maxEventsPerIp <= 0) {
|
|
729
|
+
throw new Error("Server rateLimit maxEventsPerIp must be a positive number.");
|
|
730
|
+
}
|
|
731
|
+
if (action !== "throttle" && action !== "disconnect") {
|
|
732
|
+
throw new Error('Server rateLimit action must be either "throttle" or "disconnect".');
|
|
733
|
+
}
|
|
734
|
+
if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
|
|
735
|
+
throw new Error("Server rateLimit throttleMs must be a positive number.");
|
|
736
|
+
}
|
|
737
|
+
if (!Number.isFinite(maxThrottleMs) || maxThrottleMs <= 0) {
|
|
738
|
+
throw new Error("Server rateLimit maxThrottleMs must be a positive number.");
|
|
739
|
+
}
|
|
740
|
+
if (maxThrottleMs < throttleMs) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
"Server rateLimit maxThrottleMs must be greater than or equal to throttleMs."
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
if (!Number.isInteger(disconnectAfterViolations) || disconnectAfterViolations <= 0) {
|
|
746
|
+
throw new Error(
|
|
747
|
+
"Server rateLimit disconnectAfterViolations must be a positive integer."
|
|
748
|
+
);
|
|
749
|
+
}
|
|
750
|
+
if (!Number.isInteger(disconnectCode) || disconnectCode < 1e3 || disconnectCode > 4999) {
|
|
751
|
+
throw new Error("Server rateLimit disconnectCode must be a valid WebSocket close code.");
|
|
752
|
+
}
|
|
753
|
+
return {
|
|
754
|
+
enabled: rateLimitOptions?.enabled ?? true,
|
|
755
|
+
windowMs,
|
|
756
|
+
maxEventsPerConnection,
|
|
757
|
+
maxEventsPerIp,
|
|
758
|
+
action,
|
|
759
|
+
throttleMs,
|
|
760
|
+
maxThrottleMs,
|
|
761
|
+
disconnectAfterViolations,
|
|
762
|
+
disconnectCode,
|
|
763
|
+
disconnectReason
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
createRateLimitBucket(now) {
|
|
767
|
+
return {
|
|
768
|
+
windowStartedAt: now,
|
|
769
|
+
count: 0,
|
|
770
|
+
violationCount: 0,
|
|
771
|
+
throttleUntil: 0,
|
|
772
|
+
lastSeenAt: now
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
getOrCreateRateLimitBucket(map, key, now) {
|
|
776
|
+
const existingBucket = map.get(key);
|
|
777
|
+
if (existingBucket) {
|
|
778
|
+
return existingBucket;
|
|
779
|
+
}
|
|
780
|
+
const bucket = this.createRateLimitBucket(now);
|
|
781
|
+
map.set(key, bucket);
|
|
782
|
+
return bucket;
|
|
783
|
+
}
|
|
784
|
+
updateRateLimitBucket(bucket, now) {
|
|
785
|
+
if (now - bucket.windowStartedAt >= this.rateLimitConfig.windowMs) {
|
|
786
|
+
bucket.windowStartedAt = now;
|
|
787
|
+
bucket.count = 0;
|
|
788
|
+
bucket.violationCount = 0;
|
|
789
|
+
bucket.throttleUntil = 0;
|
|
790
|
+
}
|
|
791
|
+
bucket.count += 1;
|
|
792
|
+
bucket.lastSeenAt = now;
|
|
793
|
+
}
|
|
794
|
+
pruneRateLimitBucketMap(map, now, maxIdleMs) {
|
|
795
|
+
for (const [key, bucket] of map.entries()) {
|
|
796
|
+
if (now - bucket.lastSeenAt >= maxIdleMs) {
|
|
797
|
+
map.delete(key);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
pruneRateLimitBuckets(now) {
|
|
802
|
+
const maxIdleMs = this.rateLimitConfig.windowMs * 4;
|
|
803
|
+
this.pruneRateLimitBucketMap(this.rateLimitBucketsByClientId, now, maxIdleMs);
|
|
804
|
+
this.pruneRateLimitBucketMap(this.rateLimitBucketsByIp, now, maxIdleMs);
|
|
805
|
+
}
|
|
806
|
+
normalizeIpAddress(ipAddress) {
|
|
807
|
+
let normalized = ipAddress.trim().toLowerCase();
|
|
808
|
+
if (normalized.startsWith("::ffff:")) {
|
|
809
|
+
normalized = normalized.slice(7);
|
|
810
|
+
}
|
|
811
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
|
812
|
+
normalized = normalized.slice(1, -1);
|
|
813
|
+
}
|
|
814
|
+
const zoneIndex = normalized.indexOf("%");
|
|
815
|
+
if (zoneIndex >= 0) {
|
|
816
|
+
normalized = normalized.slice(0, zoneIndex);
|
|
817
|
+
}
|
|
818
|
+
return normalized.length > 0 ? normalized : "unknown";
|
|
819
|
+
}
|
|
820
|
+
resolveClientIp(request) {
|
|
821
|
+
const forwardedHeader = request.headers["x-forwarded-for"];
|
|
822
|
+
const forwardedValue = Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader;
|
|
823
|
+
if (typeof forwardedValue === "string") {
|
|
824
|
+
const firstForwardedIp = forwardedValue.split(",").map((item) => item.trim()).find((item) => item.length > 0);
|
|
825
|
+
if (firstForwardedIp) {
|
|
826
|
+
return this.normalizeIpAddress(firstForwardedIp);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return this.normalizeIpAddress(request.socket.remoteAddress ?? "unknown");
|
|
830
|
+
}
|
|
831
|
+
isIpStillConnected(ipAddress) {
|
|
832
|
+
for (const connectedIp of this.clientIpByClientId.values()) {
|
|
833
|
+
if (connectedIp === ipAddress) {
|
|
834
|
+
return true;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return false;
|
|
838
|
+
}
|
|
839
|
+
evaluateIncomingRateLimit(client) {
|
|
840
|
+
const noLimitDecision = {
|
|
841
|
+
shouldDisconnect: false,
|
|
842
|
+
shouldDrop: false,
|
|
843
|
+
throttleDelayMs: 0
|
|
844
|
+
};
|
|
845
|
+
if (!this.rateLimitConfig.enabled) {
|
|
846
|
+
return noLimitDecision;
|
|
847
|
+
}
|
|
848
|
+
const now = Date.now();
|
|
849
|
+
const clientBucket = this.getOrCreateRateLimitBucket(
|
|
850
|
+
this.rateLimitBucketsByClientId,
|
|
851
|
+
client.id,
|
|
852
|
+
now
|
|
853
|
+
);
|
|
854
|
+
this.updateRateLimitBucket(clientBucket, now);
|
|
855
|
+
const clientIp = this.clientIpByClientId.get(client.id);
|
|
856
|
+
const ipBucket = clientIp ? this.getOrCreateRateLimitBucket(this.rateLimitBucketsByIp, clientIp, now) : null;
|
|
857
|
+
if (ipBucket) {
|
|
858
|
+
this.updateRateLimitBucket(ipBucket, now);
|
|
859
|
+
}
|
|
860
|
+
const activeThrottleUntil = Math.max(
|
|
861
|
+
clientBucket.throttleUntil,
|
|
862
|
+
ipBucket?.throttleUntil ?? 0
|
|
863
|
+
);
|
|
864
|
+
if (activeThrottleUntil > now) {
|
|
865
|
+
return {
|
|
866
|
+
shouldDisconnect: false,
|
|
867
|
+
shouldDrop: true,
|
|
868
|
+
throttleDelayMs: 0
|
|
869
|
+
};
|
|
870
|
+
}
|
|
871
|
+
const isConnectionLimitExceeded = clientBucket.count > this.rateLimitConfig.maxEventsPerConnection;
|
|
872
|
+
const isIpLimitExceeded = ipBucket ? ipBucket.count > this.rateLimitConfig.maxEventsPerIp : false;
|
|
873
|
+
if (!isConnectionLimitExceeded && !isIpLimitExceeded) {
|
|
874
|
+
if (this.rateLimitBucketsByClientId.size > 1024 || this.rateLimitBucketsByIp.size > 1024) {
|
|
875
|
+
this.pruneRateLimitBuckets(now);
|
|
876
|
+
}
|
|
877
|
+
return noLimitDecision;
|
|
878
|
+
}
|
|
879
|
+
if (isConnectionLimitExceeded) {
|
|
880
|
+
clientBucket.violationCount += 1;
|
|
881
|
+
}
|
|
882
|
+
if (ipBucket && isIpLimitExceeded) {
|
|
883
|
+
ipBucket.violationCount += 1;
|
|
884
|
+
}
|
|
885
|
+
const violationCount = Math.max(
|
|
886
|
+
clientBucket.violationCount,
|
|
887
|
+
ipBucket?.violationCount ?? 0
|
|
888
|
+
);
|
|
889
|
+
const shouldDisconnect = this.rateLimitConfig.action === "disconnect" || violationCount >= this.rateLimitConfig.disconnectAfterViolations;
|
|
890
|
+
if (shouldDisconnect) {
|
|
891
|
+
return {
|
|
892
|
+
shouldDisconnect: true,
|
|
893
|
+
shouldDrop: true,
|
|
894
|
+
throttleDelayMs: 0
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
const throttleDelayMs = Math.min(
|
|
898
|
+
this.rateLimitConfig.maxThrottleMs,
|
|
899
|
+
Math.max(
|
|
900
|
+
this.rateLimitConfig.throttleMs,
|
|
901
|
+
this.rateLimitConfig.throttleMs * violationCount
|
|
902
|
+
)
|
|
903
|
+
);
|
|
904
|
+
const throttleUntil = now + throttleDelayMs;
|
|
905
|
+
clientBucket.throttleUntil = throttleUntil;
|
|
906
|
+
if (ipBucket) {
|
|
907
|
+
ipBucket.throttleUntil = throttleUntil;
|
|
908
|
+
}
|
|
909
|
+
return {
|
|
910
|
+
shouldDisconnect: false,
|
|
911
|
+
shouldDrop: false,
|
|
912
|
+
throttleDelayMs
|
|
913
|
+
};
|
|
914
|
+
}
|
|
576
915
|
startHeartbeatLoop() {
|
|
577
916
|
if (!this.heartbeatConfig.enabled || this.heartbeatIntervalHandle) {
|
|
578
917
|
return;
|
|
@@ -675,6 +1014,8 @@ var SecureServer = class {
|
|
|
675
1014
|
try {
|
|
676
1015
|
const clientId = randomUUID();
|
|
677
1016
|
const handshakeState = this.createServerHandshakeState();
|
|
1017
|
+
const clientIp = this.resolveClientIp(request);
|
|
1018
|
+
connectionMetadata.set("network.ip", clientIp);
|
|
678
1019
|
const client = this.createSecureServerClient(
|
|
679
1020
|
clientId,
|
|
680
1021
|
socket,
|
|
@@ -683,6 +1024,7 @@ var SecureServer = class {
|
|
|
683
1024
|
);
|
|
684
1025
|
this.clientsById.set(clientId, client);
|
|
685
1026
|
this.clientIdBySocket.set(socket, clientId);
|
|
1027
|
+
this.clientIpByClientId.set(clientId, clientIp);
|
|
686
1028
|
this.handshakeStateBySocket.set(socket, handshakeState);
|
|
687
1029
|
this.pendingPayloadsBySocket.set(socket, []);
|
|
688
1030
|
this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
|
|
@@ -717,6 +1059,35 @@ var SecureServer = class {
|
|
|
717
1059
|
}
|
|
718
1060
|
async handleIncomingMessage(client, rawData) {
|
|
719
1061
|
try {
|
|
1062
|
+
const rateLimitDecision = this.evaluateIncomingRateLimit(client);
|
|
1063
|
+
if (rateLimitDecision.shouldDisconnect) {
|
|
1064
|
+
this.notifyError(
|
|
1065
|
+
new Error(
|
|
1066
|
+
`Rate limit disconnect triggered for client ${client.id}.`
|
|
1067
|
+
)
|
|
1068
|
+
);
|
|
1069
|
+
if (client.socket.readyState === WebSocket.OPEN || client.socket.readyState === WebSocket.CONNECTING) {
|
|
1070
|
+
client.socket.close(
|
|
1071
|
+
this.rateLimitConfig.disconnectCode,
|
|
1072
|
+
this.rateLimitConfig.disconnectReason
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (rateLimitDecision.shouldDrop) {
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
if (rateLimitDecision.throttleDelayMs > 0) {
|
|
1081
|
+
this.notifyError(
|
|
1082
|
+
new Error(
|
|
1083
|
+
`Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
|
|
1084
|
+
)
|
|
1085
|
+
);
|
|
1086
|
+
await delay(rateLimitDecision.throttleDelayMs);
|
|
1087
|
+
if (client.socket.readyState !== WebSocket.OPEN) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
720
1091
|
let envelope = null;
|
|
721
1092
|
try {
|
|
722
1093
|
envelope = parseEnvelope(rawData);
|
|
@@ -778,6 +1149,12 @@ var SecureServer = class {
|
|
|
778
1149
|
client.leaveAll();
|
|
779
1150
|
this.clientsById.delete(client.id);
|
|
780
1151
|
this.clientIdBySocket.delete(client.socket);
|
|
1152
|
+
const disconnectedIp = this.clientIpByClientId.get(client.id);
|
|
1153
|
+
this.clientIpByClientId.delete(client.id);
|
|
1154
|
+
this.rateLimitBucketsByClientId.delete(client.id);
|
|
1155
|
+
if (disconnectedIp && !this.isIpStillConnected(disconnectedIp)) {
|
|
1156
|
+
this.rateLimitBucketsByIp.delete(disconnectedIp);
|
|
1157
|
+
}
|
|
781
1158
|
this.handshakeStateBySocket.delete(client.socket);
|
|
782
1159
|
this.sharedSecretBySocket.delete(client.socket);
|
|
783
1160
|
this.encryptionKeyBySocket.delete(client.socket);
|
|
@@ -1165,6 +1542,48 @@ var SecureServer = class {
|
|
|
1165
1542
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
1166
1543
|
};
|
|
1167
1544
|
}
|
|
1545
|
+
emitLocally(event, data) {
|
|
1546
|
+
const envelope = { event, data };
|
|
1547
|
+
for (const client of this.clientsById.values()) {
|
|
1548
|
+
void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
|
|
1549
|
+
return void 0;
|
|
1550
|
+
});
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
publishAdapterMessage(message) {
|
|
1554
|
+
if (!this.adapter) {
|
|
1555
|
+
return;
|
|
1556
|
+
}
|
|
1557
|
+
let adapterMessage;
|
|
1558
|
+
if (message.scope === "room") {
|
|
1559
|
+
if (!message.room) {
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
adapterMessage = {
|
|
1563
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
1564
|
+
originServerId: this.instanceId,
|
|
1565
|
+
scope: "room",
|
|
1566
|
+
event: message.event,
|
|
1567
|
+
data: message.data,
|
|
1568
|
+
emittedAt: Date.now(),
|
|
1569
|
+
room: message.room
|
|
1570
|
+
};
|
|
1571
|
+
} else {
|
|
1572
|
+
adapterMessage = {
|
|
1573
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
1574
|
+
originServerId: this.instanceId,
|
|
1575
|
+
scope: "broadcast",
|
|
1576
|
+
event: message.event,
|
|
1577
|
+
data: message.data,
|
|
1578
|
+
emittedAt: Date.now()
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
void Promise.resolve(this.adapter.publish(adapterMessage)).catch((error) => {
|
|
1582
|
+
this.notifyError(
|
|
1583
|
+
normalizeToError(error, "Failed to publish SecureServer adapter message.")
|
|
1584
|
+
);
|
|
1585
|
+
});
|
|
1586
|
+
}
|
|
1168
1587
|
normalizeRoomName(room) {
|
|
1169
1588
|
if (typeof room !== "string") {
|
|
1170
1589
|
throw new Error("Room name must be a string.");
|
|
@@ -1230,22 +1649,29 @@ var SecureServer = class {
|
|
|
1230
1649
|
}
|
|
1231
1650
|
return roomNames.length;
|
|
1232
1651
|
}
|
|
1233
|
-
emitToRoom(room, event, data) {
|
|
1652
|
+
emitToRoom(room, event, data, replicate) {
|
|
1234
1653
|
if (isReservedEmitEvent(event)) {
|
|
1235
1654
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1236
1655
|
}
|
|
1237
1656
|
const roomMembers = this.roomMembersByName.get(room);
|
|
1238
|
-
if (
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1657
|
+
if (roomMembers && roomMembers.size > 0) {
|
|
1658
|
+
const envelope = { event, data };
|
|
1659
|
+
for (const clientId of roomMembers) {
|
|
1660
|
+
const client = this.clientsById.get(clientId);
|
|
1661
|
+
if (!client) {
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
|
|
1665
|
+
return void 0;
|
|
1666
|
+
});
|
|
1246
1667
|
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1668
|
+
}
|
|
1669
|
+
if (replicate) {
|
|
1670
|
+
this.publishAdapterMessage({
|
|
1671
|
+
scope: "room",
|
|
1672
|
+
room,
|
|
1673
|
+
event,
|
|
1674
|
+
data
|
|
1249
1675
|
});
|
|
1250
1676
|
}
|
|
1251
1677
|
}
|
|
@@ -1848,6 +2274,6 @@ var SecureClient = class {
|
|
|
1848
2274
|
}
|
|
1849
2275
|
};
|
|
1850
2276
|
|
|
1851
|
-
export { SecureClient, SecureServer };
|
|
2277
|
+
export { SecureClient, SecureServer, normalizeSecureServerAdapterMessage };
|
|
1852
2278
|
//# sourceMappingURL=index.js.map
|
|
1853
2279
|
//# sourceMappingURL=index.js.map
|