@stonyx/sockets 0.1.1-alpha.7 → 0.1.1-alpha.9
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/dist/client.d.ts +45 -0
- package/dist/client.js +174 -0
- package/dist/encryption.d.ts +4 -0
- package/dist/encryption.js +27 -0
- package/dist/handler.d.ts +5 -0
- package/{src → dist}/handler.js +1 -1
- package/dist/main.d.ts +9 -0
- package/dist/main.js +18 -0
- package/dist/server.d.ts +48 -0
- package/dist/server.js +169 -0
- package/package.json +27 -9
- package/src/client.js +0 -197
- package/src/encryption.js +0 -31
- package/src/main.js +0 -19
- package/src/server.js +0 -186
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
interface SocketMessage {
|
|
3
|
+
request: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
response?: unknown;
|
|
6
|
+
sessionKey?: string;
|
|
7
|
+
}
|
|
8
|
+
interface HandlerInstance {
|
|
9
|
+
_clientRef?: SocketClient;
|
|
10
|
+
client: (response: unknown) => void;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
export default class SocketClient {
|
|
14
|
+
static instance: SocketClient | null;
|
|
15
|
+
handlers: Record<string, HandlerInstance>;
|
|
16
|
+
reconnectCount: number;
|
|
17
|
+
_intentionalClose: boolean;
|
|
18
|
+
socket: WebSocket | null;
|
|
19
|
+
sessionKey: Buffer | null;
|
|
20
|
+
globalKey: Buffer | null;
|
|
21
|
+
encryptionEnabled: boolean;
|
|
22
|
+
_heartBeatTimer: ReturnType<typeof setTimeout> | null;
|
|
23
|
+
promise: {
|
|
24
|
+
resolve: () => void;
|
|
25
|
+
reject: (reason?: unknown) => void;
|
|
26
|
+
} | null;
|
|
27
|
+
onDisconnect: (() => void) | null;
|
|
28
|
+
onReconnecting: ((attempt: number, delay: number) => void) | null;
|
|
29
|
+
onReconnected: (() => void) | null;
|
|
30
|
+
onReconnectFailed: (() => void) | null;
|
|
31
|
+
constructor();
|
|
32
|
+
init(): Promise<void>;
|
|
33
|
+
discoverHandlers(): Promise<void>;
|
|
34
|
+
connect(): Promise<void>;
|
|
35
|
+
onMessage(payload: Buffer | string): void;
|
|
36
|
+
send(payload: SocketMessage, useGlobalKey?: boolean): void;
|
|
37
|
+
heartBeat(): void;
|
|
38
|
+
nextHeartBeat(): void;
|
|
39
|
+
onClose(): void;
|
|
40
|
+
close(): void;
|
|
41
|
+
getReconnectDelay(): number;
|
|
42
|
+
reconnect(): Promise<void>;
|
|
43
|
+
reset(): void;
|
|
44
|
+
}
|
|
45
|
+
export {};
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import config from 'stonyx/config';
|
|
3
|
+
import log from 'stonyx/log';
|
|
4
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
+
import { sleep } from '@stonyx/utils/promise';
|
|
6
|
+
import { encrypt, decrypt, deriveKey } from './encryption.js';
|
|
7
|
+
export default class SocketClient {
|
|
8
|
+
static instance;
|
|
9
|
+
handlers = {};
|
|
10
|
+
reconnectCount = 0;
|
|
11
|
+
_intentionalClose = false;
|
|
12
|
+
socket = null;
|
|
13
|
+
sessionKey = null;
|
|
14
|
+
globalKey = null;
|
|
15
|
+
encryptionEnabled = false;
|
|
16
|
+
_heartBeatTimer = null;
|
|
17
|
+
promise = null;
|
|
18
|
+
onDisconnect = null;
|
|
19
|
+
onReconnecting = null;
|
|
20
|
+
onReconnected = null;
|
|
21
|
+
onReconnectFailed = null;
|
|
22
|
+
constructor() {
|
|
23
|
+
if (SocketClient.instance)
|
|
24
|
+
return SocketClient.instance;
|
|
25
|
+
SocketClient.instance = this;
|
|
26
|
+
}
|
|
27
|
+
async init() {
|
|
28
|
+
await this.discoverHandlers();
|
|
29
|
+
const { encryption, authKey } = config.sockets;
|
|
30
|
+
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
31
|
+
if (this.encryptionEnabled) {
|
|
32
|
+
this.globalKey = deriveKey(authKey);
|
|
33
|
+
}
|
|
34
|
+
return this.connect();
|
|
35
|
+
}
|
|
36
|
+
async discoverHandlers() {
|
|
37
|
+
const { handlerDir } = config.sockets;
|
|
38
|
+
await forEachFileImport(handlerDir, (HandlerClassUntyped, { name }) => {
|
|
39
|
+
const HandlerClass = HandlerClassUntyped;
|
|
40
|
+
const instance = new HandlerClass();
|
|
41
|
+
if (typeof instance.client === 'function') {
|
|
42
|
+
instance._clientRef = this;
|
|
43
|
+
this.handlers[name] = instance;
|
|
44
|
+
}
|
|
45
|
+
}, { ignoreAccessFailure: true });
|
|
46
|
+
}
|
|
47
|
+
async connect() {
|
|
48
|
+
this.sessionKey = null;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const { address, authKey, authData } = config.sockets;
|
|
51
|
+
this.promise = { resolve, reject };
|
|
52
|
+
log.socket(`Connecting to remote server: ${address}`);
|
|
53
|
+
const socket = new WebSocket(address);
|
|
54
|
+
this.socket = socket;
|
|
55
|
+
socket.on('message', (data) => this.onMessage(data));
|
|
56
|
+
socket.on('close', () => this.onClose());
|
|
57
|
+
socket.on('error', () => {
|
|
58
|
+
log.socket(`Error connecting to socket server`);
|
|
59
|
+
reject('Error connecting to socket server');
|
|
60
|
+
});
|
|
61
|
+
socket.on('open', () => {
|
|
62
|
+
this._intentionalClose = false;
|
|
63
|
+
this.reconnectCount = 0;
|
|
64
|
+
this.send({ request: 'auth', data: { authKey, ...authData } }, true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
onMessage(payload) {
|
|
69
|
+
try {
|
|
70
|
+
let parsed;
|
|
71
|
+
if (this.encryptionEnabled) {
|
|
72
|
+
const key = this.sessionKey || this.globalKey;
|
|
73
|
+
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
74
|
+
parsed = JSON.parse(decrypt(raw, key));
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
const raw = Buffer.isBuffer(payload) ? payload.toString() : payload;
|
|
78
|
+
parsed = JSON.parse(raw);
|
|
79
|
+
}
|
|
80
|
+
const { request, response, sessionKey } = parsed;
|
|
81
|
+
if (request === 'auth') {
|
|
82
|
+
if (sessionKey && this.encryptionEnabled) {
|
|
83
|
+
this.sessionKey = Buffer.from(sessionKey, 'base64');
|
|
84
|
+
}
|
|
85
|
+
this.nextHeartBeat();
|
|
86
|
+
}
|
|
87
|
+
if (request === 'heartBeat') {
|
|
88
|
+
this.nextHeartBeat();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const handler = this.handlers[request];
|
|
92
|
+
if (!handler) {
|
|
93
|
+
log.socket(`Call to invalid handler: ${request}`);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
handler.client.call({ ...handler, client: this }, response);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
log.socket(`Invalid payload received from remote server`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
send(payload, useGlobalKey = false) {
|
|
103
|
+
if (this.encryptionEnabled) {
|
|
104
|
+
const key = useGlobalKey ? this.globalKey : this.sessionKey;
|
|
105
|
+
const data = encrypt(JSON.stringify(payload), key);
|
|
106
|
+
this.socket.send(data);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
this.socket.send(JSON.stringify(payload));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
heartBeat() {
|
|
113
|
+
this.send({ request: 'heartBeat' });
|
|
114
|
+
}
|
|
115
|
+
nextHeartBeat() {
|
|
116
|
+
const { heartBeatInterval } = config.sockets;
|
|
117
|
+
this._heartBeatTimer = setTimeout(() => this.heartBeat(), heartBeatInterval);
|
|
118
|
+
}
|
|
119
|
+
onClose() {
|
|
120
|
+
log.socket('Disconnected from remote server');
|
|
121
|
+
if (this._heartBeatTimer)
|
|
122
|
+
clearTimeout(this._heartBeatTimer);
|
|
123
|
+
this.onDisconnect?.();
|
|
124
|
+
if (!this._intentionalClose) {
|
|
125
|
+
this.reconnect();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
close() {
|
|
129
|
+
this._intentionalClose = true;
|
|
130
|
+
if (this._heartBeatTimer)
|
|
131
|
+
clearTimeout(this._heartBeatTimer);
|
|
132
|
+
if (this.socket)
|
|
133
|
+
this.socket.close();
|
|
134
|
+
}
|
|
135
|
+
getReconnectDelay() {
|
|
136
|
+
const { reconnectBaseDelay = 1000, reconnectMaxDelay = 60000, } = config.sockets;
|
|
137
|
+
const exponential = reconnectBaseDelay * Math.pow(2, this.reconnectCount - 1);
|
|
138
|
+
const capped = Math.min(exponential, reconnectMaxDelay);
|
|
139
|
+
const jitter = Math.floor(Math.random() * 1000);
|
|
140
|
+
return capped + jitter;
|
|
141
|
+
}
|
|
142
|
+
async reconnect() {
|
|
143
|
+
const { maxReconnectAttempts = Infinity } = config.sockets;
|
|
144
|
+
this.reconnectCount++;
|
|
145
|
+
if (this.reconnectCount > maxReconnectAttempts) {
|
|
146
|
+
log.socket('Max reconnect attempts exceeded');
|
|
147
|
+
this.onReconnectFailed?.();
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const delay = this.getReconnectDelay();
|
|
151
|
+
this.onReconnecting?.(this.reconnectCount, delay);
|
|
152
|
+
log.socket(`Reconnecting (attempt ${this.reconnectCount}, delay ${delay}ms)`);
|
|
153
|
+
await sleep(delay / 1000);
|
|
154
|
+
try {
|
|
155
|
+
await this.connect();
|
|
156
|
+
this.onReconnected?.();
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// onClose will fire and trigger the next reconnect attempt
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
reset() {
|
|
163
|
+
this.close();
|
|
164
|
+
this.handlers = {};
|
|
165
|
+
this.sessionKey = null;
|
|
166
|
+
this.reconnectCount = 0;
|
|
167
|
+
this._intentionalClose = false;
|
|
168
|
+
this.onDisconnect = null;
|
|
169
|
+
this.onReconnecting = null;
|
|
170
|
+
this.onReconnected = null;
|
|
171
|
+
this.onReconnectFailed = null;
|
|
172
|
+
SocketClient.instance = null;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
const ALGORITHM = 'aes-256-gcm';
|
|
3
|
+
const IV_LENGTH = 12;
|
|
4
|
+
const TAG_LENGTH = 16;
|
|
5
|
+
export function encrypt(data, key) {
|
|
6
|
+
const iv = crypto.randomBytes(IV_LENGTH);
|
|
7
|
+
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
8
|
+
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
|
|
9
|
+
const tag = cipher.getAuthTag();
|
|
10
|
+
return Buffer.concat([iv, tag, encrypted]);
|
|
11
|
+
}
|
|
12
|
+
export function decrypt(buffer, key) {
|
|
13
|
+
if (typeof buffer === 'string')
|
|
14
|
+
buffer = Buffer.from(buffer, 'base64');
|
|
15
|
+
const iv = buffer.subarray(0, IV_LENGTH);
|
|
16
|
+
const tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
17
|
+
const encrypted = buffer.subarray(IV_LENGTH + TAG_LENGTH);
|
|
18
|
+
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
19
|
+
decipher.setAuthTag(tag);
|
|
20
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
|
21
|
+
}
|
|
22
|
+
export function generateSessionKey() {
|
|
23
|
+
return crypto.randomBytes(32);
|
|
24
|
+
}
|
|
25
|
+
export function deriveKey(authKey) {
|
|
26
|
+
return crypto.scryptSync(authKey, 'stonyx-sockets', 32);
|
|
27
|
+
}
|
package/{src → dist}/handler.js
RENAMED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as SocketServer } from './server.js';
|
|
2
|
+
export { default as SocketClient } from './client.js';
|
|
3
|
+
export { default as Handler } from './handler.js';
|
|
4
|
+
export default class Sockets {
|
|
5
|
+
static instance: Sockets | null;
|
|
6
|
+
constructor();
|
|
7
|
+
init(): Promise<void>;
|
|
8
|
+
reset(): void;
|
|
9
|
+
}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { default as SocketServer } from './server.js';
|
|
2
|
+
export { default as SocketClient } from './client.js';
|
|
3
|
+
export { default as Handler } from './handler.js';
|
|
4
|
+
export default class Sockets {
|
|
5
|
+
static instance;
|
|
6
|
+
constructor() {
|
|
7
|
+
if (Sockets.instance)
|
|
8
|
+
return Sockets.instance;
|
|
9
|
+
Sockets.instance = this;
|
|
10
|
+
}
|
|
11
|
+
async init() {
|
|
12
|
+
// Handler discovery is deferred to SocketServer.init() / SocketClient.init()
|
|
13
|
+
// This entry point satisfies Stonyx module auto-initialization
|
|
14
|
+
}
|
|
15
|
+
reset() {
|
|
16
|
+
Sockets.instance = null;
|
|
17
|
+
}
|
|
18
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
interface SocketMessage {
|
|
3
|
+
request: string;
|
|
4
|
+
data?: unknown;
|
|
5
|
+
response?: unknown;
|
|
6
|
+
sessionKey?: string;
|
|
7
|
+
}
|
|
8
|
+
interface ConnectedClient {
|
|
9
|
+
id: number;
|
|
10
|
+
ip: string;
|
|
11
|
+
__authenticated: boolean;
|
|
12
|
+
__sessionKey?: Buffer;
|
|
13
|
+
meta?: Record<string, unknown>;
|
|
14
|
+
send(payload: SocketMessage, keyOverride?: Buffer): void;
|
|
15
|
+
close(): void;
|
|
16
|
+
terminate(): void;
|
|
17
|
+
on(event: string, handler: (...args: unknown[]) => void): void;
|
|
18
|
+
}
|
|
19
|
+
interface HandlerInstance {
|
|
20
|
+
_serverRef?: SocketServer;
|
|
21
|
+
server: (data: unknown, client: ConnectedClient) => unknown | Promise<unknown>;
|
|
22
|
+
constructor: {
|
|
23
|
+
skipAuth: boolean;
|
|
24
|
+
};
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export default class SocketServer {
|
|
28
|
+
static instance: SocketServer | null;
|
|
29
|
+
clientMap: Map<number, ConnectedClient>;
|
|
30
|
+
handlers: Record<string, HandlerInstance>;
|
|
31
|
+
wss: WebSocketServer | null;
|
|
32
|
+
encryptionEnabled: boolean;
|
|
33
|
+
globalKey: Buffer | null;
|
|
34
|
+
onClientDisconnect: ((client: ConnectedClient) => void) | null;
|
|
35
|
+
constructor();
|
|
36
|
+
init(): Promise<void>;
|
|
37
|
+
discoverHandlers(): Promise<void>;
|
|
38
|
+
validateAuthHandler(): void;
|
|
39
|
+
onMessage(payload: Buffer | string, client: ConnectedClient): Promise<void>;
|
|
40
|
+
prepareSend(client: ConnectedClient, ws: WebSocket): void;
|
|
41
|
+
handleDisconnect(client: ConnectedClient): void;
|
|
42
|
+
sendTo(clientId: number, request: string, response: unknown): void;
|
|
43
|
+
sendToByMeta(key: string, value: unknown, request: string, response: unknown): boolean;
|
|
44
|
+
broadcast(request: string, response: unknown): void;
|
|
45
|
+
close(): void;
|
|
46
|
+
reset(): void;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { WebSocketServer } from 'ws';
|
|
2
|
+
import config from 'stonyx/config';
|
|
3
|
+
import log from 'stonyx/log';
|
|
4
|
+
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
+
import { encrypt, decrypt, deriveKey, generateSessionKey } from './encryption.js';
|
|
6
|
+
let clientId = 0;
|
|
7
|
+
export default class SocketServer {
|
|
8
|
+
static instance;
|
|
9
|
+
clientMap = new Map();
|
|
10
|
+
handlers = {};
|
|
11
|
+
wss = null;
|
|
12
|
+
encryptionEnabled = false;
|
|
13
|
+
globalKey = null;
|
|
14
|
+
onClientDisconnect = null;
|
|
15
|
+
constructor() {
|
|
16
|
+
if (SocketServer.instance)
|
|
17
|
+
return SocketServer.instance;
|
|
18
|
+
SocketServer.instance = this;
|
|
19
|
+
}
|
|
20
|
+
async init() {
|
|
21
|
+
await this.discoverHandlers();
|
|
22
|
+
this.validateAuthHandler();
|
|
23
|
+
const { port, encryption, authKey } = config.sockets;
|
|
24
|
+
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
25
|
+
if (this.encryptionEnabled) {
|
|
26
|
+
this.globalKey = deriveKey(authKey);
|
|
27
|
+
}
|
|
28
|
+
const wss = new WebSocketServer({ port });
|
|
29
|
+
this.wss = wss;
|
|
30
|
+
log.socket(`WebSocket server is listening on port ${port}`);
|
|
31
|
+
wss.on('connection', (ws, request) => {
|
|
32
|
+
const { remoteAddress } = request.socket;
|
|
33
|
+
log.socket(`[${remoteAddress}] Client connected`);
|
|
34
|
+
const client = ws;
|
|
35
|
+
client.id = ++clientId;
|
|
36
|
+
client.ip = remoteAddress || '';
|
|
37
|
+
client.__authenticated = false;
|
|
38
|
+
this.prepareSend(client, ws);
|
|
39
|
+
ws.on('message', (payload) => this.onMessage(payload, client));
|
|
40
|
+
ws.on('close', () => this.handleDisconnect(client));
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
async discoverHandlers() {
|
|
44
|
+
const { handlerDir } = config.sockets;
|
|
45
|
+
await forEachFileImport(handlerDir, (HandlerClassUntyped, { name }) => {
|
|
46
|
+
const HandlerClass = HandlerClassUntyped;
|
|
47
|
+
const instance = new HandlerClass();
|
|
48
|
+
if (typeof instance.server === 'function') {
|
|
49
|
+
instance._serverRef = this;
|
|
50
|
+
this.handlers[name] = instance;
|
|
51
|
+
}
|
|
52
|
+
}, { ignoreAccessFailure: true });
|
|
53
|
+
}
|
|
54
|
+
validateAuthHandler() {
|
|
55
|
+
if (!this.handlers.auth) {
|
|
56
|
+
throw new Error('SocketServer requires an "auth" handler with a server() method');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
async onMessage(payload, client) {
|
|
60
|
+
try {
|
|
61
|
+
let parsed;
|
|
62
|
+
if (this.encryptionEnabled) {
|
|
63
|
+
const key = client.__authenticated ? client.__sessionKey : this.globalKey;
|
|
64
|
+
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
65
|
+
parsed = JSON.parse(decrypt(raw, key));
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
parsed = JSON.parse(typeof payload === 'string' ? payload : payload.toString());
|
|
69
|
+
}
|
|
70
|
+
const { request, data } = parsed;
|
|
71
|
+
// Built-in heartbeat - no handler needed
|
|
72
|
+
if (request === 'heartBeat') {
|
|
73
|
+
if (client.__authenticated)
|
|
74
|
+
client.send({ request: 'heartBeat', response: true });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const handler = this.handlers[request];
|
|
78
|
+
if (!handler) {
|
|
79
|
+
log.socket(`Invalid request received: ${request}`);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (request !== 'auth' && !handler.constructor.skipAuth && !client.__authenticated) {
|
|
83
|
+
log.socket(`Rejected unauthenticated request: ${request}`);
|
|
84
|
+
client.close();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const response = await handler.server(data, client);
|
|
88
|
+
if (response === undefined || response === null)
|
|
89
|
+
return;
|
|
90
|
+
if (request === 'auth' && response) {
|
|
91
|
+
client.__authenticated = true;
|
|
92
|
+
if (this.encryptionEnabled) {
|
|
93
|
+
const sessionKey = generateSessionKey();
|
|
94
|
+
client.__sessionKey = sessionKey;
|
|
95
|
+
client.send({ request, response, sessionKey: sessionKey.toString('base64') }, this.globalKey);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
client.send({ request, response });
|
|
99
|
+
}
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
client.send({ request, response });
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
log.socket(`Invalid payload from client`);
|
|
106
|
+
if (config.debug)
|
|
107
|
+
console.error(error);
|
|
108
|
+
client.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
prepareSend(client, ws) {
|
|
112
|
+
const socketSend = ws.send.bind(ws);
|
|
113
|
+
const server = this;
|
|
114
|
+
client.send = (payload, keyOverride) => {
|
|
115
|
+
if (server.encryptionEnabled) {
|
|
116
|
+
const key = keyOverride || client.__sessionKey;
|
|
117
|
+
const data = encrypt(JSON.stringify(payload), key);
|
|
118
|
+
socketSend(data);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
socketSend(JSON.stringify(payload));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
handleDisconnect(client) {
|
|
126
|
+
const { ip } = client;
|
|
127
|
+
log.socket(`[${ip}] Client disconnected`);
|
|
128
|
+
this.clientMap.delete(client.id);
|
|
129
|
+
this.onClientDisconnect?.(client);
|
|
130
|
+
}
|
|
131
|
+
sendTo(clientId, request, response) {
|
|
132
|
+
const client = this.clientMap.get(clientId);
|
|
133
|
+
if (!client)
|
|
134
|
+
return;
|
|
135
|
+
client.send({ request, response });
|
|
136
|
+
}
|
|
137
|
+
sendToByMeta(key, value, request, response) {
|
|
138
|
+
for (const [, client] of this.clientMap) {
|
|
139
|
+
if (client.meta?.[key] === value && client.__authenticated) {
|
|
140
|
+
client.send({ request, response });
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
broadcast(request, response) {
|
|
147
|
+
for (const [, client] of this.clientMap) {
|
|
148
|
+
if (client.__authenticated) {
|
|
149
|
+
client.send({ request, response });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
close() {
|
|
154
|
+
if (this.wss) {
|
|
155
|
+
for (const client of this.wss.clients) {
|
|
156
|
+
client.terminate();
|
|
157
|
+
}
|
|
158
|
+
this.wss.close();
|
|
159
|
+
this.wss = null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
reset() {
|
|
163
|
+
this.close();
|
|
164
|
+
this.clientMap.clear();
|
|
165
|
+
this.handlers = {};
|
|
166
|
+
clientId = 0;
|
|
167
|
+
SocketServer.instance = null;
|
|
168
|
+
}
|
|
169
|
+
}
|
package/package.json
CHANGED
|
@@ -4,21 +4,34 @@
|
|
|
4
4
|
"stonyx-async",
|
|
5
5
|
"stonyx-module"
|
|
6
6
|
],
|
|
7
|
-
"version": "0.1.1-alpha.
|
|
7
|
+
"version": "0.1.1-alpha.9",
|
|
8
8
|
"description": "WebSocket server and client module for the Stonyx framework",
|
|
9
|
-
"main": "
|
|
9
|
+
"main": "dist/main.js",
|
|
10
|
+
"types": "dist/main.d.ts",
|
|
10
11
|
"type": "module",
|
|
11
12
|
"files": [
|
|
12
|
-
"
|
|
13
|
+
"dist",
|
|
13
14
|
"config",
|
|
14
15
|
"LICENSE.md",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
17
18
|
"exports": {
|
|
18
|
-
".":
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/main.d.ts",
|
|
21
|
+
"default": "./dist/main.js"
|
|
22
|
+
},
|
|
23
|
+
"./server": {
|
|
24
|
+
"types": "./dist/server.d.ts",
|
|
25
|
+
"default": "./dist/server.js"
|
|
26
|
+
},
|
|
27
|
+
"./client": {
|
|
28
|
+
"types": "./dist/client.d.ts",
|
|
29
|
+
"default": "./dist/client.js"
|
|
30
|
+
},
|
|
31
|
+
"./handler": {
|
|
32
|
+
"types": "./dist/handler.d.ts",
|
|
33
|
+
"default": "./dist/handler.js"
|
|
34
|
+
}
|
|
22
35
|
},
|
|
23
36
|
"publishConfig": {
|
|
24
37
|
"access": "public",
|
|
@@ -43,10 +56,15 @@
|
|
|
43
56
|
},
|
|
44
57
|
"devDependencies": {
|
|
45
58
|
"@stonyx/utils": "0.2.3-beta.7",
|
|
59
|
+
"@types/node": "^25.5.2",
|
|
60
|
+
"@types/ws": "^8.18.1",
|
|
46
61
|
"qunit": "^2.24.1",
|
|
47
|
-
"sinon": "^21.0.0"
|
|
62
|
+
"sinon": "^21.0.0",
|
|
63
|
+
"typescript": "^5.8.3"
|
|
48
64
|
},
|
|
49
65
|
"scripts": {
|
|
50
|
-
"
|
|
66
|
+
"build": "tsc",
|
|
67
|
+
"build:test": "tsc -p tsconfig.test.json",
|
|
68
|
+
"test": "pnpm build && pnpm build:test && stonyx test 'dist-test/test/**/*-test.js'"
|
|
51
69
|
}
|
|
52
70
|
}
|
package/src/client.js
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { WebSocket } from 'ws';
|
|
2
|
-
import config from 'stonyx/config';
|
|
3
|
-
import log from 'stonyx/log';
|
|
4
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
-
import { sleep } from '@stonyx/utils/promise';
|
|
6
|
-
import { encrypt, decrypt, deriveKey } from './encryption.js';
|
|
7
|
-
|
|
8
|
-
export default class SocketClient {
|
|
9
|
-
handlers = {};
|
|
10
|
-
reconnectCount = 0;
|
|
11
|
-
_intentionalClose = false;
|
|
12
|
-
|
|
13
|
-
onDisconnect = null;
|
|
14
|
-
onReconnecting = null;
|
|
15
|
-
onReconnected = null;
|
|
16
|
-
onReconnectFailed = null;
|
|
17
|
-
|
|
18
|
-
constructor() {
|
|
19
|
-
if (SocketClient.instance) return SocketClient.instance;
|
|
20
|
-
SocketClient.instance = this;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
async init() {
|
|
24
|
-
await this.discoverHandlers();
|
|
25
|
-
|
|
26
|
-
const { encryption, authKey } = config.sockets;
|
|
27
|
-
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
28
|
-
|
|
29
|
-
if (this.encryptionEnabled) {
|
|
30
|
-
this.globalKey = deriveKey(authKey);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
return this.connect();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
async discoverHandlers() {
|
|
37
|
-
const { handlerDir } = config.sockets;
|
|
38
|
-
|
|
39
|
-
await forEachFileImport(handlerDir, (HandlerClass, { name }) => {
|
|
40
|
-
const instance = new HandlerClass();
|
|
41
|
-
|
|
42
|
-
if (typeof instance.client === 'function') {
|
|
43
|
-
instance._clientRef = this;
|
|
44
|
-
this.handlers[name] = instance;
|
|
45
|
-
}
|
|
46
|
-
}, { ignoreAccessFailure: true });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async connect() {
|
|
50
|
-
return new Promise((resolve, reject) => {
|
|
51
|
-
const { address, authKey, authData } = config.sockets;
|
|
52
|
-
this.promise = { resolve, reject };
|
|
53
|
-
|
|
54
|
-
log.socket(`Connecting to remote server: ${address}`);
|
|
55
|
-
const socket = new WebSocket(address);
|
|
56
|
-
this.socket = socket;
|
|
57
|
-
|
|
58
|
-
socket.onmessage = this.onMessage.bind(this);
|
|
59
|
-
socket.onclose = this.onClose.bind(this);
|
|
60
|
-
socket.onerror = event => {
|
|
61
|
-
log.socket(`Error connecting to socket server`);
|
|
62
|
-
reject('Error connecting to socket server');
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
socket.onopen = () => {
|
|
66
|
-
this._intentionalClose = false;
|
|
67
|
-
this.reconnectCount = 0;
|
|
68
|
-
this.send({ request: 'auth', data: { authKey, ...authData } }, true);
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
onMessage({ data: payload }) {
|
|
74
|
-
try {
|
|
75
|
-
let parsed;
|
|
76
|
-
|
|
77
|
-
if (this.encryptionEnabled) {
|
|
78
|
-
const key = this.sessionKey || this.globalKey;
|
|
79
|
-
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
80
|
-
parsed = JSON.parse(decrypt(raw, key));
|
|
81
|
-
} else {
|
|
82
|
-
const raw = Buffer.isBuffer(payload) ? payload.toString() : payload;
|
|
83
|
-
parsed = JSON.parse(raw);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const { request, response, sessionKey } = parsed;
|
|
87
|
-
|
|
88
|
-
if (request === 'auth') {
|
|
89
|
-
if (sessionKey && this.encryptionEnabled) {
|
|
90
|
-
this.sessionKey = Buffer.from(sessionKey, 'base64');
|
|
91
|
-
}
|
|
92
|
-
this.nextHeartBeat();
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (request === 'heartBeat') {
|
|
96
|
-
this.nextHeartBeat();
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const handler = this.handlers[request];
|
|
101
|
-
|
|
102
|
-
if (!handler) {
|
|
103
|
-
log.socket(`Call to invalid handler: ${request}`);
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
handler.client.call({ client: this, ...handler }, response);
|
|
108
|
-
} catch (error) {
|
|
109
|
-
log.socket(`Invalid payload received from remote server`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
send(payload, useGlobalKey = false) {
|
|
114
|
-
if (this.encryptionEnabled) {
|
|
115
|
-
const key = useGlobalKey ? this.globalKey : this.sessionKey;
|
|
116
|
-
const data = encrypt(JSON.stringify(payload), key);
|
|
117
|
-
this.socket.send(data);
|
|
118
|
-
} else {
|
|
119
|
-
this.socket.send(JSON.stringify(payload));
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
heartBeat() {
|
|
124
|
-
this.send({ request: 'heartBeat' });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
nextHeartBeat() {
|
|
128
|
-
this._heartBeatTimer = setTimeout(() => this.heartBeat(), config.sockets.heartBeatInterval);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
onClose() {
|
|
132
|
-
log.socket('Disconnected from remote server');
|
|
133
|
-
if (this._heartBeatTimer) clearTimeout(this._heartBeatTimer);
|
|
134
|
-
|
|
135
|
-
this.onDisconnect?.();
|
|
136
|
-
|
|
137
|
-
if (!this._intentionalClose) {
|
|
138
|
-
this.reconnect();
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
close() {
|
|
143
|
-
this._intentionalClose = true;
|
|
144
|
-
if (this._heartBeatTimer) clearTimeout(this._heartBeatTimer);
|
|
145
|
-
if (this.socket) this.socket.close();
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
getReconnectDelay() {
|
|
149
|
-
const {
|
|
150
|
-
reconnectBaseDelay = 1000,
|
|
151
|
-
reconnectMaxDelay = 60000,
|
|
152
|
-
} = config.sockets;
|
|
153
|
-
|
|
154
|
-
const exponential = reconnectBaseDelay * Math.pow(2, this.reconnectCount - 1);
|
|
155
|
-
const capped = Math.min(exponential, reconnectMaxDelay);
|
|
156
|
-
const jitter = Math.floor(Math.random() * 1000);
|
|
157
|
-
return capped + jitter;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
async reconnect() {
|
|
161
|
-
const { maxReconnectAttempts = Infinity } = config.sockets;
|
|
162
|
-
|
|
163
|
-
this.reconnectCount++;
|
|
164
|
-
|
|
165
|
-
if (this.reconnectCount > maxReconnectAttempts) {
|
|
166
|
-
log.socket('Max reconnect attempts exceeded');
|
|
167
|
-
this.onReconnectFailed?.();
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const delay = this.getReconnectDelay();
|
|
172
|
-
this.onReconnecting?.(this.reconnectCount, delay);
|
|
173
|
-
log.socket(`Reconnecting (attempt ${this.reconnectCount}, delay ${delay}ms)`);
|
|
174
|
-
|
|
175
|
-
await sleep(delay / 1000);
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
await this.connect();
|
|
179
|
-
this.onReconnected?.();
|
|
180
|
-
} catch {
|
|
181
|
-
// onClose will fire and trigger the next reconnect attempt
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
reset() {
|
|
186
|
-
this.close();
|
|
187
|
-
this.handlers = {};
|
|
188
|
-
this.sessionKey = null;
|
|
189
|
-
this.reconnectCount = 0;
|
|
190
|
-
this._intentionalClose = false;
|
|
191
|
-
this.onDisconnect = null;
|
|
192
|
-
this.onReconnecting = null;
|
|
193
|
-
this.onReconnected = null;
|
|
194
|
-
this.onReconnectFailed = null;
|
|
195
|
-
SocketClient.instance = null;
|
|
196
|
-
}
|
|
197
|
-
}
|
package/src/encryption.js
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
const ALGORITHM = 'aes-256-gcm';
|
|
4
|
-
const IV_LENGTH = 12;
|
|
5
|
-
const TAG_LENGTH = 16;
|
|
6
|
-
|
|
7
|
-
export function encrypt(data, key) {
|
|
8
|
-
const iv = crypto.randomBytes(IV_LENGTH);
|
|
9
|
-
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
|
|
10
|
-
const encrypted = Buffer.concat([cipher.update(data, 'utf8'), cipher.final()]);
|
|
11
|
-
const tag = cipher.getAuthTag();
|
|
12
|
-
return Buffer.concat([iv, tag, encrypted]);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function decrypt(buffer, key) {
|
|
16
|
-
if (typeof buffer === 'string') buffer = Buffer.from(buffer, 'base64');
|
|
17
|
-
const iv = buffer.subarray(0, IV_LENGTH);
|
|
18
|
-
const tag = buffer.subarray(IV_LENGTH, IV_LENGTH + TAG_LENGTH);
|
|
19
|
-
const encrypted = buffer.subarray(IV_LENGTH + TAG_LENGTH);
|
|
20
|
-
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
|
|
21
|
-
decipher.setAuthTag(tag);
|
|
22
|
-
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString('utf8');
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export function generateSessionKey() {
|
|
26
|
-
return crypto.randomBytes(32);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function deriveKey(authKey) {
|
|
30
|
-
return crypto.scryptSync(authKey, 'stonyx-sockets', 32);
|
|
31
|
-
}
|
package/src/main.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
export { default as SocketServer } from './server.js';
|
|
2
|
-
export { default as SocketClient } from './client.js';
|
|
3
|
-
export { default as Handler } from './handler.js';
|
|
4
|
-
|
|
5
|
-
export default class Sockets {
|
|
6
|
-
constructor() {
|
|
7
|
-
if (Sockets.instance) return Sockets.instance;
|
|
8
|
-
Sockets.instance = this;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
async init() {
|
|
12
|
-
// Handler discovery is deferred to SocketServer.init() / SocketClient.init()
|
|
13
|
-
// This entry point satisfies Stonyx module auto-initialization
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
reset() {
|
|
17
|
-
Sockets.instance = null;
|
|
18
|
-
}
|
|
19
|
-
}
|
package/src/server.js
DELETED
|
@@ -1,186 +0,0 @@
|
|
|
1
|
-
import { WebSocketServer } from 'ws';
|
|
2
|
-
import config from 'stonyx/config';
|
|
3
|
-
import log from 'stonyx/log';
|
|
4
|
-
import { forEachFileImport } from '@stonyx/utils/file';
|
|
5
|
-
import { encrypt, decrypt, deriveKey, generateSessionKey } from './encryption.js';
|
|
6
|
-
|
|
7
|
-
let clientId = 0;
|
|
8
|
-
|
|
9
|
-
export default class SocketServer {
|
|
10
|
-
clientMap = new Map();
|
|
11
|
-
handlers = {};
|
|
12
|
-
|
|
13
|
-
constructor() {
|
|
14
|
-
if (SocketServer.instance) return SocketServer.instance;
|
|
15
|
-
SocketServer.instance = this;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async init() {
|
|
19
|
-
await this.discoverHandlers();
|
|
20
|
-
this.validateAuthHandler();
|
|
21
|
-
|
|
22
|
-
const { port, encryption, authKey } = config.sockets;
|
|
23
|
-
this.encryptionEnabled = encryption === 'true' || encryption === true;
|
|
24
|
-
|
|
25
|
-
if (this.encryptionEnabled) {
|
|
26
|
-
this.globalKey = deriveKey(authKey);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const wss = new WebSocketServer({ port });
|
|
30
|
-
this.wss = wss;
|
|
31
|
-
|
|
32
|
-
log.socket(`WebSocket server is listening on port ${port}`);
|
|
33
|
-
|
|
34
|
-
wss.on('connection', (client, request) => {
|
|
35
|
-
const { remoteAddress } = request.socket;
|
|
36
|
-
log.socket(`[${remoteAddress}] Client connected`);
|
|
37
|
-
client.id = ++clientId;
|
|
38
|
-
client.ip = remoteAddress;
|
|
39
|
-
client.__authenticated = false;
|
|
40
|
-
this.prepareSend(client);
|
|
41
|
-
|
|
42
|
-
client.on('message', payload => this.onMessage(payload, client));
|
|
43
|
-
client.on('close', () => this.handleDisconnect(client));
|
|
44
|
-
});
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async discoverHandlers() {
|
|
48
|
-
const { handlerDir } = config.sockets;
|
|
49
|
-
|
|
50
|
-
await forEachFileImport(handlerDir, (HandlerClass, { name }) => {
|
|
51
|
-
const instance = new HandlerClass();
|
|
52
|
-
|
|
53
|
-
if (typeof instance.server === 'function') {
|
|
54
|
-
instance._serverRef = this;
|
|
55
|
-
this.handlers[name] = instance;
|
|
56
|
-
}
|
|
57
|
-
}, { ignoreAccessFailure: true });
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
validateAuthHandler() {
|
|
61
|
-
if (!this.handlers.auth) {
|
|
62
|
-
throw new Error('SocketServer requires an "auth" handler with a server() method');
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async onMessage(payload, client) {
|
|
67
|
-
try {
|
|
68
|
-
let parsed;
|
|
69
|
-
|
|
70
|
-
if (this.encryptionEnabled) {
|
|
71
|
-
const key = client.__authenticated ? client.__sessionKey : this.globalKey;
|
|
72
|
-
const raw = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
73
|
-
parsed = JSON.parse(decrypt(raw, key));
|
|
74
|
-
} else {
|
|
75
|
-
parsed = JSON.parse(payload);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const { request, data } = parsed;
|
|
79
|
-
|
|
80
|
-
// Built-in heartbeat — no handler needed
|
|
81
|
-
if (request === 'heartBeat') {
|
|
82
|
-
if (client.__authenticated) client.send({ request: 'heartBeat', response: true });
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const handler = this.handlers[request];
|
|
87
|
-
|
|
88
|
-
if (!handler) {
|
|
89
|
-
log.socket(`Invalid request received: ${request}`);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (request !== 'auth' && !handler.constructor.skipAuth && !client.__authenticated) {
|
|
94
|
-
log.socket(`Rejected unauthenticated request: ${request}`);
|
|
95
|
-
client.close();
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const response = await handler.server(data, client);
|
|
100
|
-
if (response === undefined || response === null) return;
|
|
101
|
-
|
|
102
|
-
if (request === 'auth' && response) {
|
|
103
|
-
client.__authenticated = true;
|
|
104
|
-
|
|
105
|
-
if (this.encryptionEnabled) {
|
|
106
|
-
const sessionKey = generateSessionKey();
|
|
107
|
-
client.__sessionKey = sessionKey;
|
|
108
|
-
client.send({ request, response, sessionKey: sessionKey.toString('base64') }, this.globalKey);
|
|
109
|
-
} else {
|
|
110
|
-
client.send({ request, response });
|
|
111
|
-
}
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
client.send({ request, response });
|
|
116
|
-
} catch (error) {
|
|
117
|
-
log.socket(`Invalid payload from client`);
|
|
118
|
-
if (config.debug) console.error(error);
|
|
119
|
-
client.close();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
prepareSend(client) {
|
|
124
|
-
const { send: socketSend } = client;
|
|
125
|
-
const server = this;
|
|
126
|
-
|
|
127
|
-
client.send = (payload, keyOverride) => {
|
|
128
|
-
if (server.encryptionEnabled) {
|
|
129
|
-
const key = keyOverride || client.__sessionKey;
|
|
130
|
-
const data = encrypt(JSON.stringify(payload), key);
|
|
131
|
-
socketSend.bind(client)(data);
|
|
132
|
-
} else {
|
|
133
|
-
socketSend.bind(client)(JSON.stringify(payload));
|
|
134
|
-
}
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
handleDisconnect(client) {
|
|
139
|
-
const { ip } = client;
|
|
140
|
-
log.socket(`[${ip}] Client disconnected`);
|
|
141
|
-
this.clientMap.delete(client.id);
|
|
142
|
-
this.onClientDisconnect?.(client);
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
sendTo(clientId, request, response) {
|
|
146
|
-
const client = this.clientMap.get(clientId);
|
|
147
|
-
if (!client) return;
|
|
148
|
-
client.send({ request, response });
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
sendToByMeta(key, value, request, response) {
|
|
152
|
-
for (const [, client] of this.clientMap) {
|
|
153
|
-
if (client.meta?.[key] === value && client.__authenticated) {
|
|
154
|
-
client.send({ request, response });
|
|
155
|
-
return true;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
broadcast(request, response) {
|
|
162
|
-
for (const [, client] of this.clientMap) {
|
|
163
|
-
if (client.__authenticated) {
|
|
164
|
-
client.send({ request, response });
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
close() {
|
|
170
|
-
if (this.wss) {
|
|
171
|
-
for (const client of this.wss.clients) {
|
|
172
|
-
client.terminate();
|
|
173
|
-
}
|
|
174
|
-
this.wss.close();
|
|
175
|
-
this.wss = null;
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
reset() {
|
|
180
|
-
this.close();
|
|
181
|
-
this.clientMap.clear();
|
|
182
|
-
this.handlers = {};
|
|
183
|
-
clientId = 0;
|
|
184
|
-
SocketServer.instance = null;
|
|
185
|
-
}
|
|
186
|
-
}
|