@aetherframework/websocket 1.0.1 → 1.0.3
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/examples/basic-server.js +95 -0
- package/examples/chat-server.js +416 -0
- package/examples/client-test.js +58 -0
- package/examples/realtime-api.js +98 -0
- package/examples//342/200/214stress-test-server.js +495 -0
- package/package.json +3 -1
- package/src/core/ConnectionManager.js +213 -0
- package/src/core/FrameParser.js +115 -0
- package/src/core/HandshakeHandler.js +93 -0
- package/src/core/ProtocolHandler.js +186 -0
- package/src/core/WebSocketFactory.js +293 -0
- package/src/drivers/http-driver.js +576 -0
- package/src/drivers/index.js +29 -0
- package/src/drivers/memory-driver.js +422 -0
- package/src/drivers/tcp-driver.js +471 -0
- package/src/drivers/tls-driver.js +502 -0
- package/src/middleware/auth-middleware.js +37 -0
- package/src/middleware/broadcast-manager.js +173 -0
- package/src/middleware/compression.js +194 -0
- package/src/middleware/message-logger.js +322 -0
- package/src/middleware/rate-limiter.js +142 -0
- package/src/utils/config-loader.js +183 -0
- package/src/utils/connection-pool.js +110 -0
- package/src/utils/error-handler.js +59 -0
- package/src/utils/frame-encoder.js +211 -0
- package/src/utils/heartbeat-manager.js +91 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/drivers/tls-driver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import tls from 'tls';
|
|
9
|
+
import crypto from 'crypto';
|
|
10
|
+
import EventEmitter from 'events';
|
|
11
|
+
import FrameEncoder from '../utils/frame-encoder.js';
|
|
12
|
+
import HandshakeHandler from '../core/HandshakeHandler.js';
|
|
13
|
+
|
|
14
|
+
class TLSDriver extends EventEmitter {
|
|
15
|
+
constructor(config = {}) {
|
|
16
|
+
super();
|
|
17
|
+
this.config = {
|
|
18
|
+
port: 443,
|
|
19
|
+
host: '0.0.0.0',
|
|
20
|
+
tlsOptions: {
|
|
21
|
+
key: null,
|
|
22
|
+
cert: null,
|
|
23
|
+
ca: null,
|
|
24
|
+
rejectUnauthorized: false,
|
|
25
|
+
requestCert: false,
|
|
26
|
+
...config.tlsOptions
|
|
27
|
+
},
|
|
28
|
+
maxPayload: config.maxPayload || 1024 * 1024,
|
|
29
|
+
...config
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
this.server = null;
|
|
33
|
+
this.connections = new Map();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Create a TLS WebSocket server
|
|
38
|
+
* @param {Object} options - Server options
|
|
39
|
+
* @returns {Promise<Object>} Server instance
|
|
40
|
+
*/
|
|
41
|
+
async createServer(options = {}) {
|
|
42
|
+
return new Promise((resolve, reject) => {
|
|
43
|
+
try {
|
|
44
|
+
const tlsOptions = {
|
|
45
|
+
...this.config.tlsOptions,
|
|
46
|
+
...options.tlsOptions
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// Validate TLS options
|
|
50
|
+
if (!tlsOptions.key || !tlsOptions.cert) {
|
|
51
|
+
throw new Error('TLS requires key and certificate');
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
this.server = tls.createServer(tlsOptions, (socket) => {
|
|
55
|
+
this._handleConnection(socket);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const serverOptions = {
|
|
59
|
+
host: options.host || this.config.host,
|
|
60
|
+
port: options.port || this.config.port,
|
|
61
|
+
...options
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
this.server.listen(serverOptions, () => {
|
|
65
|
+
const address = this.server.address();
|
|
66
|
+
this.emit('server:listening', {
|
|
67
|
+
host: address.address,
|
|
68
|
+
port: address.port,
|
|
69
|
+
family: address.family,
|
|
70
|
+
secure: true
|
|
71
|
+
});
|
|
72
|
+
resolve({
|
|
73
|
+
type: 'tls',
|
|
74
|
+
address: address,
|
|
75
|
+
close: () => this.closeServer()
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
this.server.on('error', (error) => {
|
|
80
|
+
this.emit('error', {
|
|
81
|
+
type: 'server_error',
|
|
82
|
+
message: 'TLS server error',
|
|
83
|
+
error
|
|
84
|
+
});
|
|
85
|
+
reject(error);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.server.on('secureConnection', (socket) => {
|
|
89
|
+
const cipher = socket.getCipher();
|
|
90
|
+
this.emit('secure:connection', {
|
|
91
|
+
cipher: cipher.name,
|
|
92
|
+
version: cipher.version,
|
|
93
|
+
authorized: socket.authorized
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
reject(error);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Handle new TLS connection
|
|
105
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
106
|
+
* @private
|
|
107
|
+
*/
|
|
108
|
+
_handleConnection(socket) {
|
|
109
|
+
const connectionId = crypto.randomUUID();
|
|
110
|
+
let buffer = Buffer.alloc(0);
|
|
111
|
+
let handshakeComplete = false;
|
|
112
|
+
|
|
113
|
+
// Set socket options
|
|
114
|
+
socket.setNoDelay(true);
|
|
115
|
+
socket.setKeepAlive(true, 10000);
|
|
116
|
+
socket.setTimeout(this.config.socketTimeout || 30000);
|
|
117
|
+
|
|
118
|
+
// Handle incoming data
|
|
119
|
+
socket.on('data', (chunk) => {
|
|
120
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
121
|
+
|
|
122
|
+
if (!handshakeComplete) {
|
|
123
|
+
this._handleHandshake(socket, buffer, connectionId)
|
|
124
|
+
.then((handshakeResult) => {
|
|
125
|
+
if (handshakeResult.success) {
|
|
126
|
+
handshakeComplete = true;
|
|
127
|
+
buffer = buffer.slice(handshakeResult.bytesRead);
|
|
128
|
+
this._setupWebSocketConnection(socket, connectionId, handshakeResult);
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
.catch((error) => {
|
|
132
|
+
this.emit('error', {
|
|
133
|
+
type: 'handshake_error',
|
|
134
|
+
message: 'TLS handshake failed',
|
|
135
|
+
error,
|
|
136
|
+
connectionId
|
|
137
|
+
});
|
|
138
|
+
socket.end();
|
|
139
|
+
});
|
|
140
|
+
} else {
|
|
141
|
+
this._handleWebSocketData(socket, buffer, connectionId);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Handle socket events
|
|
146
|
+
socket.on('close', () => {
|
|
147
|
+
this.emit('close', this.connections.get(connectionId), 1006, 'Connection closed');
|
|
148
|
+
this._removeConnection(connectionId);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
socket.on('error', (error) => {
|
|
152
|
+
this.emit('error', {
|
|
153
|
+
type: 'socket_error',
|
|
154
|
+
message: 'TLS socket error',
|
|
155
|
+
error,
|
|
156
|
+
connectionId
|
|
157
|
+
});
|
|
158
|
+
this._removeConnection(connectionId);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
socket.on('timeout', () => {
|
|
162
|
+
socket.end();
|
|
163
|
+
this._removeConnection(connectionId);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Handle WebSocket handshake over TLS
|
|
169
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
170
|
+
* @param {Buffer} buffer - Received data
|
|
171
|
+
* @param {string} connectionId - Connection ID
|
|
172
|
+
* @returns {Promise<Object>} Handshake result
|
|
173
|
+
* @private
|
|
174
|
+
*/
|
|
175
|
+
async _handleHandshake(socket, buffer, connectionId) {
|
|
176
|
+
const request = HandshakeHandler.parseRequest(buffer);
|
|
177
|
+
if (!request) {
|
|
178
|
+
return { success: false, bytesRead: 0 };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!HandshakeHandler.isValidUpgrade(request)) {
|
|
182
|
+
socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
|
|
183
|
+
socket.end();
|
|
184
|
+
return { success: false, bytesRead: buffer.length };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const selectedProtocol = this._negotiateProtocol(request.headers);
|
|
188
|
+
const response = HandshakeHandler.createResponse(
|
|
189
|
+
request.headers['sec-websocket-key'],
|
|
190
|
+
selectedProtocol
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
socket.write(response);
|
|
194
|
+
|
|
195
|
+
const connection = {
|
|
196
|
+
id: connectionId,
|
|
197
|
+
socket: socket,
|
|
198
|
+
readyState: 1,
|
|
199
|
+
protocol: selectedProtocol,
|
|
200
|
+
remoteAddress: socket.remoteAddress,
|
|
201
|
+
remotePort: socket.remotePort,
|
|
202
|
+
cipher: socket.getCipher(),
|
|
203
|
+
authorized: socket.authorized,
|
|
204
|
+
headers: request.headers
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
this.connections.set(connectionId, connection);
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
success: true,
|
|
211
|
+
bytesRead: request.rawHeaderSize,
|
|
212
|
+
connection: connection
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Negotiate subprotocol
|
|
218
|
+
* @param {Object} headers - HTTP headers
|
|
219
|
+
* @returns {string|null} Selected protocol
|
|
220
|
+
* @private
|
|
221
|
+
*/
|
|
222
|
+
_negotiateProtocol(headers) {
|
|
223
|
+
const clientProtocols = headers['sec-websocket-protocol'];
|
|
224
|
+
if (!clientProtocols) return null;
|
|
225
|
+
|
|
226
|
+
const supportedProtocols = this.config.supportedProtocols || [];
|
|
227
|
+
const requestedProtocols = clientProtocols.split(',').map(p => p.trim());
|
|
228
|
+
|
|
229
|
+
for (const protocol of requestedProtocols) {
|
|
230
|
+
if (supportedProtocols.includes(protocol)) {
|
|
231
|
+
return protocol;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Set up WebSocket connection after handshake
|
|
240
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
241
|
+
* @param {string} connectionId - Connection ID
|
|
242
|
+
* @param {Object} handshakeResult - Handshake result
|
|
243
|
+
* @private
|
|
244
|
+
*/
|
|
245
|
+
_setupWebSocketConnection(socket, connectionId, handshakeResult) {
|
|
246
|
+
const connection = handshakeResult.connection;
|
|
247
|
+
|
|
248
|
+
// Emit connection event
|
|
249
|
+
this.emit('connection', connection);
|
|
250
|
+
|
|
251
|
+
// Set up ping/pong heartbeat
|
|
252
|
+
if (this.config.pingInterval) {
|
|
253
|
+
const pingInterval = setInterval(() => {
|
|
254
|
+
if (connection.readyState === 1) {
|
|
255
|
+
this._sendPing(socket);
|
|
256
|
+
} else {
|
|
257
|
+
clearInterval(pingInterval);
|
|
258
|
+
}
|
|
259
|
+
}, this.config.pingInterval);
|
|
260
|
+
|
|
261
|
+
connection.pingInterval = pingInterval;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Handle WebSocket data frames
|
|
267
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
268
|
+
* @param {Buffer} buffer - Data buffer
|
|
269
|
+
* @param {string} connectionId - Connection ID
|
|
270
|
+
* @private
|
|
271
|
+
*/
|
|
272
|
+
_handleWebSocketData(socket, buffer, connectionId) {
|
|
273
|
+
const connection = this.connections.get(connectionId);
|
|
274
|
+
if (!connection) return;
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
// Parse frames using FrameEncoder (assuming it has parse method)
|
|
278
|
+
const frames = FrameEncoder.parse(buffer, this.config.maxPayload);
|
|
279
|
+
|
|
280
|
+
for (const frame of frames) {
|
|
281
|
+
switch (frame.opcode) {
|
|
282
|
+
case 0x1: // Text frame
|
|
283
|
+
const text = frame.payload.toString('utf8');
|
|
284
|
+
this.emit('message', connection, text, false);
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
case 0x2: // Binary frame
|
|
288
|
+
this.emit('message', connection, frame.payload, true);
|
|
289
|
+
break;
|
|
290
|
+
|
|
291
|
+
case 0x8: // Close frame
|
|
292
|
+
const code = frame.payload.length >= 2 ?
|
|
293
|
+
frame.payload.readUInt16BE(0) : 1000;
|
|
294
|
+
const reason = frame.payload.length > 2 ?
|
|
295
|
+
frame.payload.slice(2).toString('utf8') : '';
|
|
296
|
+
this._sendCloseFrame(socket, code, reason);
|
|
297
|
+
connection.readyState = 3;
|
|
298
|
+
this.emit('close', connection, code, reason);
|
|
299
|
+
socket.end();
|
|
300
|
+
break;
|
|
301
|
+
|
|
302
|
+
case 0x9: // Ping frame
|
|
303
|
+
this._sendPong(socket, frame.payload);
|
|
304
|
+
break;
|
|
305
|
+
|
|
306
|
+
case 0xA: // Pong frame
|
|
307
|
+
this.emit('pong', connection);
|
|
308
|
+
break;
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
this.emit('error', {
|
|
313
|
+
type: 'frame_parse_error',
|
|
314
|
+
message: 'Failed to parse WebSocket frame',
|
|
315
|
+
error,
|
|
316
|
+
connectionId
|
|
317
|
+
});
|
|
318
|
+
this._sendCloseFrame(socket, 1002, 'Protocol error');
|
|
319
|
+
socket.end();
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Send ping frame
|
|
325
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
326
|
+
* @private
|
|
327
|
+
*/
|
|
328
|
+
_sendPing(socket) {
|
|
329
|
+
const pingFrame = FrameEncoder.createPingFrame();
|
|
330
|
+
socket.write(pingFrame);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Send pong frame
|
|
335
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
336
|
+
* @param {Buffer} payload - Ping payload to echo back
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
_sendPong(socket, payload) {
|
|
340
|
+
const pongFrame = FrameEncoder.createPongFrame(payload);
|
|
341
|
+
socket.write(pongFrame);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Send close frame
|
|
346
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
347
|
+
* @param {number} code - Close code
|
|
348
|
+
* @param {string} reason - Close reason
|
|
349
|
+
* @private
|
|
350
|
+
*/
|
|
351
|
+
_sendCloseFrame(socket, code, reason) {
|
|
352
|
+
const closeFrame = FrameEncoder.createCloseFrame(code, reason);
|
|
353
|
+
socket.write(closeFrame);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Remove connection
|
|
358
|
+
* @param {string} connectionId - Connection ID
|
|
359
|
+
* @private
|
|
360
|
+
*/
|
|
361
|
+
_removeConnection(connectionId) {
|
|
362
|
+
const connection = this.connections.get(connectionId);
|
|
363
|
+
if (connection) {
|
|
364
|
+
if (connection.pingInterval) {
|
|
365
|
+
clearInterval(connection.pingInterval);
|
|
366
|
+
}
|
|
367
|
+
this.connections.delete(connectionId);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Create a secure WebSocket client
|
|
373
|
+
* @param {string} url - WebSocket URL (wss://)
|
|
374
|
+
* @param {Object} options - Client options
|
|
375
|
+
* @returns {Promise<Object>} Client connection
|
|
376
|
+
*/
|
|
377
|
+
async createClient(url, options = {}) {
|
|
378
|
+
const urlObj = new URL(url);
|
|
379
|
+
const host = urlObj.hostname;
|
|
380
|
+
const port = parseInt(urlObj.port) || 443;
|
|
381
|
+
const path = urlObj.pathname + urlObj.search;
|
|
382
|
+
|
|
383
|
+
const tlsOptions = {
|
|
384
|
+
host,
|
|
385
|
+
port,
|
|
386
|
+
rejectUnauthorized: false,
|
|
387
|
+
...options.tlsOptions
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
return new Promise((resolve, reject) => {
|
|
391
|
+
const socket = tls.connect(tlsOptions, () => {
|
|
392
|
+
// Send WebSocket handshake
|
|
393
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
394
|
+
const handshake = [
|
|
395
|
+
`GET ${path} HTTP/1.1`,
|
|
396
|
+
`Host: ${host}:${port}`,
|
|
397
|
+
'Upgrade: websocket',
|
|
398
|
+
'Connection: Upgrade',
|
|
399
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
400
|
+
'Sec-WebSocket-Version: 13',
|
|
401
|
+
'Sec-WebSocket-Protocol: tls'
|
|
402
|
+
].join('\r\n') + '\r\n\r\n';
|
|
403
|
+
|
|
404
|
+
socket.write(handshake);
|
|
405
|
+
|
|
406
|
+
let buffer = Buffer.alloc(0);
|
|
407
|
+
|
|
408
|
+
const onData = (chunk) => {
|
|
409
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
410
|
+
|
|
411
|
+
const response = buffer.toString('utf8');
|
|
412
|
+
if (response.includes('\r\n\r\n')) {
|
|
413
|
+
socket.removeListener('data', onData);
|
|
414
|
+
|
|
415
|
+
if (response.includes('101 Switching Protocols')) {
|
|
416
|
+
const connection = {
|
|
417
|
+
id: crypto.randomUUID(),
|
|
418
|
+
socket: socket,
|
|
419
|
+
readyState: 1,
|
|
420
|
+
send: (data) => this._sendData(socket, data),
|
|
421
|
+
close: (code, reason) => {
|
|
422
|
+
this._sendCloseFrame(socket, code, reason);
|
|
423
|
+
socket.end();
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
socket.on('data', (data) => {
|
|
428
|
+
this._handleWebSocketData(socket, data, connection.id);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
resolve(connection);
|
|
432
|
+
} else {
|
|
433
|
+
reject(new Error('TLS handshake failed'));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
socket.on('data', onData);
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
socket.on('error', reject);
|
|
442
|
+
socket.on('timeout', () => reject(new Error('TLS connection timeout')));
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Send data through TLS socket
|
|
448
|
+
* @param {tls.TLSSocket} socket - TLS socket
|
|
449
|
+
* @param {string|Buffer} data - Data to send
|
|
450
|
+
* @private
|
|
451
|
+
*/
|
|
452
|
+
_sendData(socket, data) {
|
|
453
|
+
const frame = FrameEncoder.encode(
|
|
454
|
+
0x1, // Text frame opcode
|
|
455
|
+
Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8')
|
|
456
|
+
);
|
|
457
|
+
socket.write(frame);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Close the TLS server
|
|
462
|
+
* @returns {Promise<void>}
|
|
463
|
+
*/
|
|
464
|
+
async closeServer() {
|
|
465
|
+
return new Promise((resolve) => {
|
|
466
|
+
if (this.server) {
|
|
467
|
+
// Close all connections
|
|
468
|
+
this.connections.forEach((connection) => {
|
|
469
|
+
if (connection.readyState === 1) {
|
|
470
|
+
this._sendCloseFrame(connection.socket, 1001, 'Server going away');
|
|
471
|
+
connection.socket.end();
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
this.server.close(() => {
|
|
476
|
+
this.connections.clear();
|
|
477
|
+
resolve();
|
|
478
|
+
});
|
|
479
|
+
} else {
|
|
480
|
+
resolve();
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get server statistics
|
|
487
|
+
* @returns {Object} Statistics object
|
|
488
|
+
*/
|
|
489
|
+
getStats() {
|
|
490
|
+
return {
|
|
491
|
+
connections: this.connections.size,
|
|
492
|
+
uptime: this.server ? Date.now() - this.server.startTime : 0,
|
|
493
|
+
host: this.config.host,
|
|
494
|
+
port: this.config.port,
|
|
495
|
+
driver: 'tls',
|
|
496
|
+
secure: true,
|
|
497
|
+
cipherSuites: this.config.tlsOptions.ciphers || 'DEFAULT'
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
export default TLSDriver;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/middleware/auth-middleware
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Create auth middleware
|
|
10
|
+
* @param {Function} verifyFn - Async function to verify token
|
|
11
|
+
* @returns {Function} Middleware function
|
|
12
|
+
*/
|
|
13
|
+
export function createAuthMiddleware(verifyFn) {
|
|
14
|
+
return async (connection, next) => {
|
|
15
|
+
const token = connection.headers['sec-websocket-protocol'] ||
|
|
16
|
+
connection.url?.searchParams?.get('token');
|
|
17
|
+
|
|
18
|
+
if (!token) {
|
|
19
|
+
connection.close(4001, 'Authentication required');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const user = await verifyFn(token);
|
|
25
|
+
if (!user) {
|
|
26
|
+
connection.close(4003, 'Invalid token');
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
connection.user = user;
|
|
30
|
+
next();
|
|
31
|
+
} catch (error) {
|
|
32
|
+
connection.close(4002, 'Auth error');
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export default createAuthMiddleware;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/middleware/broadcast-manager
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BroadcastManager {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.options = {
|
|
12
|
+
maxRooms: options.maxRooms || 10000,
|
|
13
|
+
batchSize: options.batchSize || 100, // Process sends in batches to avoid event loop blocking
|
|
14
|
+
...options
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// Map<roomName, Set<connectionId>>
|
|
18
|
+
this.rooms = new Map();
|
|
19
|
+
// Map<connectionId, Set<roomName>> - for quick lookup of which rooms a conn belongs to
|
|
20
|
+
this.connRooms = new Map();
|
|
21
|
+
|
|
22
|
+
this.stats = {
|
|
23
|
+
broadcasts: 0,
|
|
24
|
+
messagesSent: 0
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Middleware factory
|
|
30
|
+
*/
|
|
31
|
+
middleware() {
|
|
32
|
+
return (connection, next) => {
|
|
33
|
+
// Attach room methods to connection object
|
|
34
|
+
connection.join = (room) => this.addToRoom(connection.id, room);
|
|
35
|
+
connection.leave = (room) => this.removeFromRoom(connection.id, room);
|
|
36
|
+
connection.rooms = () => this.getConnRooms(connection.id);
|
|
37
|
+
|
|
38
|
+
// Cleanup on close
|
|
39
|
+
const originalClose = connection.close.bind(connection);
|
|
40
|
+
connection.close = (code, reason) => {
|
|
41
|
+
this.removeAllFromRooms(connection.id);
|
|
42
|
+
return originalClose(code, reason);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
next();
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add connection to a room
|
|
51
|
+
* @param {string} connId
|
|
52
|
+
* @param {string} room
|
|
53
|
+
*/
|
|
54
|
+
addToRoom(connId, room) {
|
|
55
|
+
if (!this.rooms.has(room)) {
|
|
56
|
+
if (this.rooms.size >= this.options.maxRooms) {
|
|
57
|
+
throw new Error(`Max rooms limit (${this.options.maxRooms}) reached`);
|
|
58
|
+
}
|
|
59
|
+
this.rooms.set(room, new Set());
|
|
60
|
+
}
|
|
61
|
+
this.rooms.get(room).add(connId);
|
|
62
|
+
|
|
63
|
+
if (!this.connRooms.has(connId)) {
|
|
64
|
+
this.connRooms.set(connId, new Set());
|
|
65
|
+
}
|
|
66
|
+
this.connRooms.get(connId).add(room);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Remove connection from a room
|
|
71
|
+
* @param {string} connId
|
|
72
|
+
* @param {string} room
|
|
73
|
+
*/
|
|
74
|
+
removeFromRoom(connId, room) {
|
|
75
|
+
const roomSet = this.rooms.get(room);
|
|
76
|
+
if (roomSet) {
|
|
77
|
+
roomSet.delete(connId);
|
|
78
|
+
if (roomSet.size === 0) {
|
|
79
|
+
this.rooms.delete(room);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const userRooms = this.connRooms.get(connId);
|
|
84
|
+
if (userRooms) {
|
|
85
|
+
userRooms.delete(room);
|
|
86
|
+
if (userRooms.size === 0) {
|
|
87
|
+
this.connRooms.delete(connId);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Remove connection from all rooms
|
|
94
|
+
* @param {string} connId
|
|
95
|
+
*/
|
|
96
|
+
removeAllFromRooms(connId) {
|
|
97
|
+
const userRooms = this.connRooms.get(connId);
|
|
98
|
+
if (userRooms) {
|
|
99
|
+
for (const room of userRooms) {
|
|
100
|
+
this.removeFromRoom(connId, room);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get rooms for a connection
|
|
107
|
+
* @param {string} connId
|
|
108
|
+
* @returns {Array<string>}
|
|
109
|
+
*/
|
|
110
|
+
getConnRooms(connId) {
|
|
111
|
+
const rooms = this.connRooms.get(connId);
|
|
112
|
+
return rooms ? Array.from(rooms) : [];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Broadcast message to a specific room
|
|
117
|
+
* @param {string} room
|
|
118
|
+
* @param {string|Buffer} data
|
|
119
|
+
* @param {Object} connectionManager - Instance to get active connections
|
|
120
|
+
*/
|
|
121
|
+
broadcastToRoom(room, data, connectionManager) {
|
|
122
|
+
const roomSet = this.rooms.get(room);
|
|
123
|
+
if (!roomSet || roomSet.size === 0) return;
|
|
124
|
+
|
|
125
|
+
this.stats.broadcasts++;
|
|
126
|
+
let sentCount = 0;
|
|
127
|
+
|
|
128
|
+
// Convert to buffer once if string to optimize memory
|
|
129
|
+
const payload = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
130
|
+
|
|
131
|
+
for (const connId of roomSet) {
|
|
132
|
+
const conn = connectionManager.getConnection(connId);
|
|
133
|
+
if (conn && conn.readyState === 1) { // OPEN
|
|
134
|
+
conn.sendBinary ? conn.sendBinary(payload) : conn.send(payload);
|
|
135
|
+
sentCount++;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.stats.messagesSent += sentCount;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Broadcast to all connected clients
|
|
144
|
+
* @param {string|Buffer} data
|
|
145
|
+
* @param {Object} connectionManager
|
|
146
|
+
*/
|
|
147
|
+
broadcastAll(data, connectionManager) {
|
|
148
|
+
const allConns = connectionManager.getAllConnections();
|
|
149
|
+
this.stats.broadcasts++;
|
|
150
|
+
let sentCount = 0;
|
|
151
|
+
|
|
152
|
+
const payload = typeof data === 'string' ? Buffer.from(data, 'utf8') : data;
|
|
153
|
+
|
|
154
|
+
for (const conn of allConns) {
|
|
155
|
+
if (conn.readyState === 1) {
|
|
156
|
+
conn.sendBinary ? conn.sendBinary(payload) : conn.send(payload);
|
|
157
|
+
sentCount++;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.stats.messagesSent += sentCount;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
getStats() {
|
|
165
|
+
return {
|
|
166
|
+
...this.stats,
|
|
167
|
+
activeRooms: this.rooms.size,
|
|
168
|
+
totalMappings: this.connRooms.size
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export default BroadcastManager;
|