@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.cjs
CHANGED
|
@@ -32,6 +32,59 @@ var DEFAULT_RECONNECT_MAX_DELAY_MS = 1e4;
|
|
|
32
32
|
var DEFAULT_RECONNECT_FACTOR = 2;
|
|
33
33
|
var DEFAULT_RECONNECT_JITTER_RATIO = 0.2;
|
|
34
34
|
var DEFAULT_RPC_TIMEOUT_MS = 5e3;
|
|
35
|
+
var DEFAULT_RATE_LIMIT_WINDOW_MS = 1e3;
|
|
36
|
+
var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION = 120;
|
|
37
|
+
var DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP = 300;
|
|
38
|
+
var DEFAULT_RATE_LIMIT_THROTTLE_MS = 150;
|
|
39
|
+
var DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS = 2e3;
|
|
40
|
+
var DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS = 4;
|
|
41
|
+
var DEFAULT_RATE_LIMIT_CLOSE_CODE = 1013;
|
|
42
|
+
var DEFAULT_RATE_LIMIT_CLOSE_REASON = "Rate limit exceeded. Please retry later.";
|
|
43
|
+
var SECURE_SERVER_ADAPTER_MESSAGE_VERSION = 1;
|
|
44
|
+
function normalizeSecureServerAdapterMessage(value) {
|
|
45
|
+
if (!isPlainObject(value)) {
|
|
46
|
+
throw new Error("SecureServer adapter message must be a plain object.");
|
|
47
|
+
}
|
|
48
|
+
if (value.version !== SECURE_SERVER_ADAPTER_MESSAGE_VERSION) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`Unsupported SecureServer adapter message version: ${String(value.version)}.`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (typeof value.originServerId !== "string" || value.originServerId.trim().length === 0) {
|
|
54
|
+
throw new Error("SecureServer adapter message originServerId must be a non-empty string.");
|
|
55
|
+
}
|
|
56
|
+
if (value.scope !== "broadcast" && value.scope !== "room") {
|
|
57
|
+
throw new Error('SecureServer adapter message scope must be either "broadcast" or "room".');
|
|
58
|
+
}
|
|
59
|
+
if (typeof value.event !== "string" || value.event.trim().length === 0) {
|
|
60
|
+
throw new Error("SecureServer adapter message event must be a non-empty string.");
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.emittedAt !== "number" || !Number.isFinite(value.emittedAt)) {
|
|
63
|
+
throw new Error("SecureServer adapter message emittedAt must be a finite number.");
|
|
64
|
+
}
|
|
65
|
+
if (value.scope === "room") {
|
|
66
|
+
if (typeof value.room !== "string" || value.room.trim().length === 0) {
|
|
67
|
+
throw new Error("SecureServer adapter message room must be a non-empty string.");
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
71
|
+
originServerId: value.originServerId,
|
|
72
|
+
scope: value.scope,
|
|
73
|
+
event: value.event,
|
|
74
|
+
data: value.data,
|
|
75
|
+
emittedAt: value.emittedAt,
|
|
76
|
+
room: value.room.trim()
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
81
|
+
originServerId: value.originServerId,
|
|
82
|
+
scope: value.scope,
|
|
83
|
+
event: value.event,
|
|
84
|
+
data: value.data,
|
|
85
|
+
emittedAt: value.emittedAt
|
|
86
|
+
};
|
|
87
|
+
}
|
|
35
88
|
function normalizeToError(error, fallbackMessage) {
|
|
36
89
|
if (error instanceof Error) {
|
|
37
90
|
return error;
|
|
@@ -65,6 +118,11 @@ function rawDataToBuffer(rawData) {
|
|
|
65
118
|
}
|
|
66
119
|
return Buffer.from(rawData);
|
|
67
120
|
}
|
|
121
|
+
function delay(ms) {
|
|
122
|
+
return new Promise((resolve) => {
|
|
123
|
+
setTimeout(resolve, ms);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
68
126
|
function isBlobValue(value) {
|
|
69
127
|
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
70
128
|
}
|
|
@@ -355,8 +413,11 @@ function decryptSerializedEnvelope(rawData, encryptionKey) {
|
|
|
355
413
|
return plaintext.toString("utf8");
|
|
356
414
|
}
|
|
357
415
|
var SecureServer = class {
|
|
416
|
+
instanceId = crypto.randomUUID();
|
|
358
417
|
socketServer;
|
|
418
|
+
adapter = null;
|
|
359
419
|
heartbeatConfig;
|
|
420
|
+
rateLimitConfig;
|
|
360
421
|
heartbeatIntervalHandle = null;
|
|
361
422
|
clientsById = /* @__PURE__ */ new Map();
|
|
362
423
|
clientIdBySocket = /* @__PURE__ */ new Map();
|
|
@@ -375,19 +436,80 @@ var SecureServer = class {
|
|
|
375
436
|
heartbeatStateBySocket = /* @__PURE__ */ new WeakMap();
|
|
376
437
|
roomMembersByName = /* @__PURE__ */ new Map();
|
|
377
438
|
roomNamesByClientId = /* @__PURE__ */ new Map();
|
|
439
|
+
clientIpByClientId = /* @__PURE__ */ new Map();
|
|
440
|
+
rateLimitBucketsByClientId = /* @__PURE__ */ new Map();
|
|
441
|
+
rateLimitBucketsByIp = /* @__PURE__ */ new Map();
|
|
378
442
|
constructor(options) {
|
|
379
|
-
const { heartbeat, ...socketServerOptions } = options;
|
|
443
|
+
const { heartbeat, rateLimit, adapter, ...socketServerOptions } = options;
|
|
380
444
|
this.heartbeatConfig = this.resolveHeartbeatConfig(heartbeat);
|
|
445
|
+
this.rateLimitConfig = this.resolveRateLimitConfig(rateLimit);
|
|
381
446
|
this.socketServer = new WebSocket.WebSocketServer(socketServerOptions);
|
|
382
447
|
this.bindSocketServerEvents();
|
|
383
448
|
this.startHeartbeatLoop();
|
|
449
|
+
if (adapter) {
|
|
450
|
+
void this.setAdapter(adapter).catch(() => {
|
|
451
|
+
return void 0;
|
|
452
|
+
});
|
|
453
|
+
}
|
|
384
454
|
}
|
|
385
455
|
get clientCount() {
|
|
386
456
|
return this.clientsById.size;
|
|
387
457
|
}
|
|
458
|
+
get serverId() {
|
|
459
|
+
return this.instanceId;
|
|
460
|
+
}
|
|
388
461
|
get clients() {
|
|
389
462
|
return this.clientsById;
|
|
390
463
|
}
|
|
464
|
+
async setAdapter(adapter) {
|
|
465
|
+
const previousAdapter = this.adapter;
|
|
466
|
+
if (previousAdapter === adapter) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
try {
|
|
470
|
+
if (previousAdapter?.detach) {
|
|
471
|
+
await Promise.resolve(previousAdapter.detach(this));
|
|
472
|
+
}
|
|
473
|
+
this.adapter = null;
|
|
474
|
+
if (!adapter) {
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
await Promise.resolve(adapter.attach(this));
|
|
478
|
+
this.adapter = adapter;
|
|
479
|
+
} catch (error) {
|
|
480
|
+
const normalizedError = normalizeToError(
|
|
481
|
+
error,
|
|
482
|
+
"Failed to set SecureServer adapter."
|
|
483
|
+
);
|
|
484
|
+
this.notifyError(normalizedError);
|
|
485
|
+
throw normalizedError;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
async handleAdapterMessage(message) {
|
|
489
|
+
try {
|
|
490
|
+
const normalizedMessage = normalizeSecureServerAdapterMessage(message);
|
|
491
|
+
if (normalizedMessage.originServerId === this.instanceId) {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (normalizedMessage.scope === "broadcast") {
|
|
495
|
+
this.emitLocally(normalizedMessage.event, normalizedMessage.data);
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
if (!normalizedMessage.room) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.emitToRoom(
|
|
502
|
+
normalizedMessage.room,
|
|
503
|
+
normalizedMessage.event,
|
|
504
|
+
normalizedMessage.data,
|
|
505
|
+
false
|
|
506
|
+
);
|
|
507
|
+
} catch (error) {
|
|
508
|
+
this.notifyError(
|
|
509
|
+
normalizeToError(error, "Failed to process SecureServer adapter message.")
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
391
513
|
on(event, handler) {
|
|
392
514
|
try {
|
|
393
515
|
if (event === "connection") {
|
|
@@ -474,12 +596,12 @@ var SecureServer = class {
|
|
|
474
596
|
if (isReservedEmitEvent(event)) {
|
|
475
597
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
476
598
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
}
|
|
599
|
+
this.emitLocally(event, data);
|
|
600
|
+
this.publishAdapterMessage({
|
|
601
|
+
scope: "broadcast",
|
|
602
|
+
event,
|
|
603
|
+
data
|
|
604
|
+
});
|
|
483
605
|
} catch (error) {
|
|
484
606
|
this.notifyError(normalizeToError(error, "Failed to emit server event."));
|
|
485
607
|
}
|
|
@@ -536,7 +658,7 @@ var SecureServer = class {
|
|
|
536
658
|
return {
|
|
537
659
|
emit: (event, data) => {
|
|
538
660
|
try {
|
|
539
|
-
this.emitToRoom(normalizedRoom, event, data);
|
|
661
|
+
this.emitToRoom(normalizedRoom, event, data, true);
|
|
540
662
|
} catch (error) {
|
|
541
663
|
this.notifyError(
|
|
542
664
|
normalizeToError(error, `Failed to emit event to room ${normalizedRoom}.`)
|
|
@@ -549,6 +671,15 @@ var SecureServer = class {
|
|
|
549
671
|
close(code = DEFAULT_CLOSE_CODE, reason = DEFAULT_CLOSE_REASON) {
|
|
550
672
|
try {
|
|
551
673
|
this.stopHeartbeatLoop();
|
|
674
|
+
const activeAdapter = this.adapter;
|
|
675
|
+
this.adapter = null;
|
|
676
|
+
if (activeAdapter?.detach) {
|
|
677
|
+
void Promise.resolve(activeAdapter.detach(this)).catch((error) => {
|
|
678
|
+
this.notifyError(
|
|
679
|
+
normalizeToError(error, "Failed to detach SecureServer adapter during close.")
|
|
680
|
+
);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
552
683
|
for (const client of this.clientsById.values()) {
|
|
553
684
|
this.rejectPendingRpcRequests(
|
|
554
685
|
client.socket,
|
|
@@ -559,6 +690,9 @@ var SecureServer = class {
|
|
|
559
690
|
client.socket.close(code, reason);
|
|
560
691
|
}
|
|
561
692
|
}
|
|
693
|
+
this.rateLimitBucketsByClientId.clear();
|
|
694
|
+
this.rateLimitBucketsByIp.clear();
|
|
695
|
+
this.clientIpByClientId.clear();
|
|
562
696
|
this.socketServer.close();
|
|
563
697
|
} catch (error) {
|
|
564
698
|
this.notifyError(normalizeToError(error, "Failed to close server."));
|
|
@@ -579,6 +713,211 @@ var SecureServer = class {
|
|
|
579
713
|
timeoutMs
|
|
580
714
|
};
|
|
581
715
|
}
|
|
716
|
+
resolveRateLimitConfig(rateLimitOptions) {
|
|
717
|
+
const windowMs = rateLimitOptions?.windowMs ?? DEFAULT_RATE_LIMIT_WINDOW_MS;
|
|
718
|
+
const maxEventsPerConnection = rateLimitOptions?.maxEventsPerConnection ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_CONNECTION;
|
|
719
|
+
const maxEventsPerIp = rateLimitOptions?.maxEventsPerIp ?? DEFAULT_RATE_LIMIT_MAX_EVENTS_PER_IP;
|
|
720
|
+
const action = rateLimitOptions?.action ?? "throttle";
|
|
721
|
+
const throttleMs = rateLimitOptions?.throttleMs ?? DEFAULT_RATE_LIMIT_THROTTLE_MS;
|
|
722
|
+
const maxThrottleMs = rateLimitOptions?.maxThrottleMs ?? DEFAULT_RATE_LIMIT_MAX_THROTTLE_MS;
|
|
723
|
+
const disconnectAfterViolations = rateLimitOptions?.disconnectAfterViolations ?? DEFAULT_RATE_LIMIT_DISCONNECT_AFTER_VIOLATIONS;
|
|
724
|
+
const disconnectCode = rateLimitOptions?.disconnectCode ?? DEFAULT_RATE_LIMIT_CLOSE_CODE;
|
|
725
|
+
const disconnectReason = rateLimitOptions?.disconnectReason ?? DEFAULT_RATE_LIMIT_CLOSE_REASON;
|
|
726
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) {
|
|
727
|
+
throw new Error("Server rateLimit windowMs must be a positive number.");
|
|
728
|
+
}
|
|
729
|
+
if (!Number.isFinite(maxEventsPerConnection) || maxEventsPerConnection <= 0) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"Server rateLimit maxEventsPerConnection must be a positive number."
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
if (!Number.isFinite(maxEventsPerIp) || maxEventsPerIp <= 0) {
|
|
735
|
+
throw new Error("Server rateLimit maxEventsPerIp must be a positive number.");
|
|
736
|
+
}
|
|
737
|
+
if (action !== "throttle" && action !== "disconnect") {
|
|
738
|
+
throw new Error('Server rateLimit action must be either "throttle" or "disconnect".');
|
|
739
|
+
}
|
|
740
|
+
if (!Number.isFinite(throttleMs) || throttleMs <= 0) {
|
|
741
|
+
throw new Error("Server rateLimit throttleMs must be a positive number.");
|
|
742
|
+
}
|
|
743
|
+
if (!Number.isFinite(maxThrottleMs) || maxThrottleMs <= 0) {
|
|
744
|
+
throw new Error("Server rateLimit maxThrottleMs must be a positive number.");
|
|
745
|
+
}
|
|
746
|
+
if (maxThrottleMs < throttleMs) {
|
|
747
|
+
throw new Error(
|
|
748
|
+
"Server rateLimit maxThrottleMs must be greater than or equal to throttleMs."
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (!Number.isInteger(disconnectAfterViolations) || disconnectAfterViolations <= 0) {
|
|
752
|
+
throw new Error(
|
|
753
|
+
"Server rateLimit disconnectAfterViolations must be a positive integer."
|
|
754
|
+
);
|
|
755
|
+
}
|
|
756
|
+
if (!Number.isInteger(disconnectCode) || disconnectCode < 1e3 || disconnectCode > 4999) {
|
|
757
|
+
throw new Error("Server rateLimit disconnectCode must be a valid WebSocket close code.");
|
|
758
|
+
}
|
|
759
|
+
return {
|
|
760
|
+
enabled: rateLimitOptions?.enabled ?? true,
|
|
761
|
+
windowMs,
|
|
762
|
+
maxEventsPerConnection,
|
|
763
|
+
maxEventsPerIp,
|
|
764
|
+
action,
|
|
765
|
+
throttleMs,
|
|
766
|
+
maxThrottleMs,
|
|
767
|
+
disconnectAfterViolations,
|
|
768
|
+
disconnectCode,
|
|
769
|
+
disconnectReason
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
createRateLimitBucket(now) {
|
|
773
|
+
return {
|
|
774
|
+
windowStartedAt: now,
|
|
775
|
+
count: 0,
|
|
776
|
+
violationCount: 0,
|
|
777
|
+
throttleUntil: 0,
|
|
778
|
+
lastSeenAt: now
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
getOrCreateRateLimitBucket(map, key, now) {
|
|
782
|
+
const existingBucket = map.get(key);
|
|
783
|
+
if (existingBucket) {
|
|
784
|
+
return existingBucket;
|
|
785
|
+
}
|
|
786
|
+
const bucket = this.createRateLimitBucket(now);
|
|
787
|
+
map.set(key, bucket);
|
|
788
|
+
return bucket;
|
|
789
|
+
}
|
|
790
|
+
updateRateLimitBucket(bucket, now) {
|
|
791
|
+
if (now - bucket.windowStartedAt >= this.rateLimitConfig.windowMs) {
|
|
792
|
+
bucket.windowStartedAt = now;
|
|
793
|
+
bucket.count = 0;
|
|
794
|
+
bucket.violationCount = 0;
|
|
795
|
+
bucket.throttleUntil = 0;
|
|
796
|
+
}
|
|
797
|
+
bucket.count += 1;
|
|
798
|
+
bucket.lastSeenAt = now;
|
|
799
|
+
}
|
|
800
|
+
pruneRateLimitBucketMap(map, now, maxIdleMs) {
|
|
801
|
+
for (const [key, bucket] of map.entries()) {
|
|
802
|
+
if (now - bucket.lastSeenAt >= maxIdleMs) {
|
|
803
|
+
map.delete(key);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
pruneRateLimitBuckets(now) {
|
|
808
|
+
const maxIdleMs = this.rateLimitConfig.windowMs * 4;
|
|
809
|
+
this.pruneRateLimitBucketMap(this.rateLimitBucketsByClientId, now, maxIdleMs);
|
|
810
|
+
this.pruneRateLimitBucketMap(this.rateLimitBucketsByIp, now, maxIdleMs);
|
|
811
|
+
}
|
|
812
|
+
normalizeIpAddress(ipAddress) {
|
|
813
|
+
let normalized = ipAddress.trim().toLowerCase();
|
|
814
|
+
if (normalized.startsWith("::ffff:")) {
|
|
815
|
+
normalized = normalized.slice(7);
|
|
816
|
+
}
|
|
817
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) {
|
|
818
|
+
normalized = normalized.slice(1, -1);
|
|
819
|
+
}
|
|
820
|
+
const zoneIndex = normalized.indexOf("%");
|
|
821
|
+
if (zoneIndex >= 0) {
|
|
822
|
+
normalized = normalized.slice(0, zoneIndex);
|
|
823
|
+
}
|
|
824
|
+
return normalized.length > 0 ? normalized : "unknown";
|
|
825
|
+
}
|
|
826
|
+
resolveClientIp(request) {
|
|
827
|
+
const forwardedHeader = request.headers["x-forwarded-for"];
|
|
828
|
+
const forwardedValue = Array.isArray(forwardedHeader) ? forwardedHeader[0] : forwardedHeader;
|
|
829
|
+
if (typeof forwardedValue === "string") {
|
|
830
|
+
const firstForwardedIp = forwardedValue.split(",").map((item) => item.trim()).find((item) => item.length > 0);
|
|
831
|
+
if (firstForwardedIp) {
|
|
832
|
+
return this.normalizeIpAddress(firstForwardedIp);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return this.normalizeIpAddress(request.socket.remoteAddress ?? "unknown");
|
|
836
|
+
}
|
|
837
|
+
isIpStillConnected(ipAddress) {
|
|
838
|
+
for (const connectedIp of this.clientIpByClientId.values()) {
|
|
839
|
+
if (connectedIp === ipAddress) {
|
|
840
|
+
return true;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
evaluateIncomingRateLimit(client) {
|
|
846
|
+
const noLimitDecision = {
|
|
847
|
+
shouldDisconnect: false,
|
|
848
|
+
shouldDrop: false,
|
|
849
|
+
throttleDelayMs: 0
|
|
850
|
+
};
|
|
851
|
+
if (!this.rateLimitConfig.enabled) {
|
|
852
|
+
return noLimitDecision;
|
|
853
|
+
}
|
|
854
|
+
const now = Date.now();
|
|
855
|
+
const clientBucket = this.getOrCreateRateLimitBucket(
|
|
856
|
+
this.rateLimitBucketsByClientId,
|
|
857
|
+
client.id,
|
|
858
|
+
now
|
|
859
|
+
);
|
|
860
|
+
this.updateRateLimitBucket(clientBucket, now);
|
|
861
|
+
const clientIp = this.clientIpByClientId.get(client.id);
|
|
862
|
+
const ipBucket = clientIp ? this.getOrCreateRateLimitBucket(this.rateLimitBucketsByIp, clientIp, now) : null;
|
|
863
|
+
if (ipBucket) {
|
|
864
|
+
this.updateRateLimitBucket(ipBucket, now);
|
|
865
|
+
}
|
|
866
|
+
const activeThrottleUntil = Math.max(
|
|
867
|
+
clientBucket.throttleUntil,
|
|
868
|
+
ipBucket?.throttleUntil ?? 0
|
|
869
|
+
);
|
|
870
|
+
if (activeThrottleUntil > now) {
|
|
871
|
+
return {
|
|
872
|
+
shouldDisconnect: false,
|
|
873
|
+
shouldDrop: true,
|
|
874
|
+
throttleDelayMs: 0
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
const isConnectionLimitExceeded = clientBucket.count > this.rateLimitConfig.maxEventsPerConnection;
|
|
878
|
+
const isIpLimitExceeded = ipBucket ? ipBucket.count > this.rateLimitConfig.maxEventsPerIp : false;
|
|
879
|
+
if (!isConnectionLimitExceeded && !isIpLimitExceeded) {
|
|
880
|
+
if (this.rateLimitBucketsByClientId.size > 1024 || this.rateLimitBucketsByIp.size > 1024) {
|
|
881
|
+
this.pruneRateLimitBuckets(now);
|
|
882
|
+
}
|
|
883
|
+
return noLimitDecision;
|
|
884
|
+
}
|
|
885
|
+
if (isConnectionLimitExceeded) {
|
|
886
|
+
clientBucket.violationCount += 1;
|
|
887
|
+
}
|
|
888
|
+
if (ipBucket && isIpLimitExceeded) {
|
|
889
|
+
ipBucket.violationCount += 1;
|
|
890
|
+
}
|
|
891
|
+
const violationCount = Math.max(
|
|
892
|
+
clientBucket.violationCount,
|
|
893
|
+
ipBucket?.violationCount ?? 0
|
|
894
|
+
);
|
|
895
|
+
const shouldDisconnect = this.rateLimitConfig.action === "disconnect" || violationCount >= this.rateLimitConfig.disconnectAfterViolations;
|
|
896
|
+
if (shouldDisconnect) {
|
|
897
|
+
return {
|
|
898
|
+
shouldDisconnect: true,
|
|
899
|
+
shouldDrop: true,
|
|
900
|
+
throttleDelayMs: 0
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
const throttleDelayMs = Math.min(
|
|
904
|
+
this.rateLimitConfig.maxThrottleMs,
|
|
905
|
+
Math.max(
|
|
906
|
+
this.rateLimitConfig.throttleMs,
|
|
907
|
+
this.rateLimitConfig.throttleMs * violationCount
|
|
908
|
+
)
|
|
909
|
+
);
|
|
910
|
+
const throttleUntil = now + throttleDelayMs;
|
|
911
|
+
clientBucket.throttleUntil = throttleUntil;
|
|
912
|
+
if (ipBucket) {
|
|
913
|
+
ipBucket.throttleUntil = throttleUntil;
|
|
914
|
+
}
|
|
915
|
+
return {
|
|
916
|
+
shouldDisconnect: false,
|
|
917
|
+
shouldDrop: false,
|
|
918
|
+
throttleDelayMs
|
|
919
|
+
};
|
|
920
|
+
}
|
|
582
921
|
startHeartbeatLoop() {
|
|
583
922
|
if (!this.heartbeatConfig.enabled || this.heartbeatIntervalHandle) {
|
|
584
923
|
return;
|
|
@@ -681,6 +1020,8 @@ var SecureServer = class {
|
|
|
681
1020
|
try {
|
|
682
1021
|
const clientId = crypto.randomUUID();
|
|
683
1022
|
const handshakeState = this.createServerHandshakeState();
|
|
1023
|
+
const clientIp = this.resolveClientIp(request);
|
|
1024
|
+
connectionMetadata.set("network.ip", clientIp);
|
|
684
1025
|
const client = this.createSecureServerClient(
|
|
685
1026
|
clientId,
|
|
686
1027
|
socket,
|
|
@@ -689,6 +1030,7 @@ var SecureServer = class {
|
|
|
689
1030
|
);
|
|
690
1031
|
this.clientsById.set(clientId, client);
|
|
691
1032
|
this.clientIdBySocket.set(socket, clientId);
|
|
1033
|
+
this.clientIpByClientId.set(clientId, clientIp);
|
|
692
1034
|
this.handshakeStateBySocket.set(socket, handshakeState);
|
|
693
1035
|
this.pendingPayloadsBySocket.set(socket, []);
|
|
694
1036
|
this.pendingRpcRequestsBySocket.set(socket, /* @__PURE__ */ new Map());
|
|
@@ -723,6 +1065,35 @@ var SecureServer = class {
|
|
|
723
1065
|
}
|
|
724
1066
|
async handleIncomingMessage(client, rawData) {
|
|
725
1067
|
try {
|
|
1068
|
+
const rateLimitDecision = this.evaluateIncomingRateLimit(client);
|
|
1069
|
+
if (rateLimitDecision.shouldDisconnect) {
|
|
1070
|
+
this.notifyError(
|
|
1071
|
+
new Error(
|
|
1072
|
+
`Rate limit disconnect triggered for client ${client.id}.`
|
|
1073
|
+
)
|
|
1074
|
+
);
|
|
1075
|
+
if (client.socket.readyState === WebSocket__default.default.OPEN || client.socket.readyState === WebSocket__default.default.CONNECTING) {
|
|
1076
|
+
client.socket.close(
|
|
1077
|
+
this.rateLimitConfig.disconnectCode,
|
|
1078
|
+
this.rateLimitConfig.disconnectReason
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
if (rateLimitDecision.shouldDrop) {
|
|
1084
|
+
return;
|
|
1085
|
+
}
|
|
1086
|
+
if (rateLimitDecision.throttleDelayMs > 0) {
|
|
1087
|
+
this.notifyError(
|
|
1088
|
+
new Error(
|
|
1089
|
+
`Rate limit throttle applied to client ${client.id} for ${rateLimitDecision.throttleDelayMs}ms.`
|
|
1090
|
+
)
|
|
1091
|
+
);
|
|
1092
|
+
await delay(rateLimitDecision.throttleDelayMs);
|
|
1093
|
+
if (client.socket.readyState !== WebSocket__default.default.OPEN) {
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
726
1097
|
let envelope = null;
|
|
727
1098
|
try {
|
|
728
1099
|
envelope = parseEnvelope(rawData);
|
|
@@ -784,6 +1155,12 @@ var SecureServer = class {
|
|
|
784
1155
|
client.leaveAll();
|
|
785
1156
|
this.clientsById.delete(client.id);
|
|
786
1157
|
this.clientIdBySocket.delete(client.socket);
|
|
1158
|
+
const disconnectedIp = this.clientIpByClientId.get(client.id);
|
|
1159
|
+
this.clientIpByClientId.delete(client.id);
|
|
1160
|
+
this.rateLimitBucketsByClientId.delete(client.id);
|
|
1161
|
+
if (disconnectedIp && !this.isIpStillConnected(disconnectedIp)) {
|
|
1162
|
+
this.rateLimitBucketsByIp.delete(disconnectedIp);
|
|
1163
|
+
}
|
|
787
1164
|
this.handshakeStateBySocket.delete(client.socket);
|
|
788
1165
|
this.sharedSecretBySocket.delete(client.socket);
|
|
789
1166
|
this.encryptionKeyBySocket.delete(client.socket);
|
|
@@ -1171,6 +1548,48 @@ var SecureServer = class {
|
|
|
1171
1548
|
leaveAll: () => this.leaveClientFromAllRooms(clientId)
|
|
1172
1549
|
};
|
|
1173
1550
|
}
|
|
1551
|
+
emitLocally(event, data) {
|
|
1552
|
+
const envelope = { event, data };
|
|
1553
|
+
for (const client of this.clientsById.values()) {
|
|
1554
|
+
void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
|
|
1555
|
+
return void 0;
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
publishAdapterMessage(message) {
|
|
1560
|
+
if (!this.adapter) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
let adapterMessage;
|
|
1564
|
+
if (message.scope === "room") {
|
|
1565
|
+
if (!message.room) {
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
adapterMessage = {
|
|
1569
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
1570
|
+
originServerId: this.instanceId,
|
|
1571
|
+
scope: "room",
|
|
1572
|
+
event: message.event,
|
|
1573
|
+
data: message.data,
|
|
1574
|
+
emittedAt: Date.now(),
|
|
1575
|
+
room: message.room
|
|
1576
|
+
};
|
|
1577
|
+
} else {
|
|
1578
|
+
adapterMessage = {
|
|
1579
|
+
version: SECURE_SERVER_ADAPTER_MESSAGE_VERSION,
|
|
1580
|
+
originServerId: this.instanceId,
|
|
1581
|
+
scope: "broadcast",
|
|
1582
|
+
event: message.event,
|
|
1583
|
+
data: message.data,
|
|
1584
|
+
emittedAt: Date.now()
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
void Promise.resolve(this.adapter.publish(adapterMessage)).catch((error) => {
|
|
1588
|
+
this.notifyError(
|
|
1589
|
+
normalizeToError(error, "Failed to publish SecureServer adapter message.")
|
|
1590
|
+
);
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1174
1593
|
normalizeRoomName(room) {
|
|
1175
1594
|
if (typeof room !== "string") {
|
|
1176
1595
|
throw new Error("Room name must be a string.");
|
|
@@ -1236,22 +1655,29 @@ var SecureServer = class {
|
|
|
1236
1655
|
}
|
|
1237
1656
|
return roomNames.length;
|
|
1238
1657
|
}
|
|
1239
|
-
emitToRoom(room, event, data) {
|
|
1658
|
+
emitToRoom(room, event, data, replicate) {
|
|
1240
1659
|
if (isReservedEmitEvent(event)) {
|
|
1241
1660
|
throw new Error(`The event "${event}" is reserved and cannot be emitted manually.`);
|
|
1242
1661
|
}
|
|
1243
1662
|
const roomMembers = this.roomMembersByName.get(room);
|
|
1244
|
-
if (
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1663
|
+
if (roomMembers && roomMembers.size > 0) {
|
|
1664
|
+
const envelope = { event, data };
|
|
1665
|
+
for (const clientId of roomMembers) {
|
|
1666
|
+
const client = this.clientsById.get(clientId);
|
|
1667
|
+
if (!client) {
|
|
1668
|
+
continue;
|
|
1669
|
+
}
|
|
1670
|
+
void this.sendOrQueuePayload(client.socket, envelope).catch(() => {
|
|
1671
|
+
return void 0;
|
|
1672
|
+
});
|
|
1252
1673
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1674
|
+
}
|
|
1675
|
+
if (replicate) {
|
|
1676
|
+
this.publishAdapterMessage({
|
|
1677
|
+
scope: "room",
|
|
1678
|
+
room,
|
|
1679
|
+
event,
|
|
1680
|
+
data
|
|
1255
1681
|
});
|
|
1256
1682
|
}
|
|
1257
1683
|
}
|
|
@@ -1856,5 +2282,6 @@ var SecureClient = class {
|
|
|
1856
2282
|
|
|
1857
2283
|
exports.SecureClient = SecureClient;
|
|
1858
2284
|
exports.SecureServer = SecureServer;
|
|
2285
|
+
exports.normalizeSecureServerAdapterMessage = normalizeSecureServerAdapterMessage;
|
|
1859
2286
|
//# sourceMappingURL=index.cjs.map
|
|
1860
2287
|
//# sourceMappingURL=index.cjs.map
|