@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.
@@ -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;