@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,471 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/src/drivers/tcp-driver
6
+ */
7
+
8
+ import net from 'net';
9
+ import crypto from 'crypto';
10
+ import EventEmitter from 'events';
11
+ import FrameEncoder from '../utils/frame-encoder.js';
12
+
13
+ class TCPDriver extends EventEmitter {
14
+ constructor(config = {}) {
15
+ super();
16
+ this.config = config;
17
+ this.server = null;
18
+ this.connections = new Map();
19
+ }
20
+
21
+ /**
22
+ * Create a TCP WebSocket server
23
+ * @param {Object} options - Server options
24
+ * @returns {Promise<Object>} Server instance
25
+ */
26
+ async createServer(options = {}) {
27
+ return new Promise((resolve, reject) => {
28
+ try {
29
+ this.server = net.createServer((socket) => {
30
+ this._handleConnection(socket);
31
+ });
32
+
33
+ const serverOptions = {
34
+ host: options.host || '0.0.0.0',
35
+ port: options.port || 8080,
36
+ ...options
37
+ };
38
+
39
+ this.server.listen(serverOptions, () => {
40
+ const address = this.server.address();
41
+ this.emit('server:listening', {
42
+ host: address.address,
43
+ port: address.port,
44
+ family: address.family
45
+ });
46
+ resolve({
47
+ type: 'tcp',
48
+ address: address,
49
+ close: () => this.closeServer()
50
+ });
51
+ });
52
+
53
+ this.server.on('error', (error) => {
54
+ this.emit('error', {
55
+ type: 'server_error',
56
+ message: 'Server error',
57
+ error
58
+ });
59
+ reject(error);
60
+ });
61
+
62
+ } catch (error) {
63
+ reject(error);
64
+ }
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Handle new TCP connection
70
+ * @param {net.Socket} socket - TCP socket
71
+ * @private
72
+ */
73
+ _handleConnection(socket) {
74
+ const connectionId = crypto.randomUUID();
75
+ let buffer = Buffer.alloc(0);
76
+ let handshakeComplete = false;
77
+
78
+ // Set socket options
79
+ socket.setNoDelay(true);
80
+ socket.setKeepAlive(true, 10000);
81
+ socket.setTimeout(this.config.socketTimeout || 30000);
82
+
83
+ // Handle incoming data
84
+ socket.on('data', (chunk) => {
85
+ buffer = Buffer.concat([buffer, chunk]);
86
+
87
+ if (!handshakeComplete) {
88
+ this._handleHandshake(socket, buffer, connectionId)
89
+ .then((handshakeResult) => {
90
+ if (handshakeResult.success) {
91
+ handshakeComplete = true;
92
+ buffer = buffer.slice(handshakeResult.bytesRead);
93
+ this._setupWebSocketConnection(socket, connectionId, handshakeResult);
94
+ }
95
+ })
96
+ .catch((error) => {
97
+ this.emit('error', {
98
+ type: 'handshake_error',
99
+ message: 'WebSocket handshake failed',
100
+ error,
101
+ connectionId
102
+ });
103
+ socket.end();
104
+ });
105
+ } else {
106
+ this._handleWebSocketData(socket, buffer, connectionId);
107
+ }
108
+ });
109
+
110
+ // Handle socket errors
111
+ socket.on('error', (error) => {
112
+ this.emit('error', {
113
+ type: 'socket_error',
114
+ message: 'Socket error',
115
+ error,
116
+ connectionId
117
+ });
118
+ this._removeConnection(connectionId);
119
+ });
120
+
121
+ // Handle socket close
122
+ socket.on('close', (hadError) => {
123
+ this.emit('close', this.connections.get(connectionId), 1006, 'Connection closed');
124
+ this._removeConnection(connectionId);
125
+ });
126
+
127
+ // Handle socket timeout
128
+ socket.on('timeout', () => {
129
+ socket.end();
130
+ this._removeConnection(connectionId);
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Handle WebSocket handshake
136
+ * @param {net.Socket} socket - TCP socket
137
+ * @param {Buffer} buffer - Received data buffer
138
+ * @param {string} connectionId - Connection ID
139
+ * @returns {Promise<Object>} Handshake result
140
+ * @private
141
+ */
142
+ async _handleHandshake(socket, buffer, connectionId) {
143
+ const request = buffer.toString('utf8');
144
+
145
+ // Check if we have a complete HTTP request
146
+ if (!request.includes('\r\n\r\n')) {
147
+ return { success: false, bytesRead: 0 };
148
+ }
149
+
150
+ const lines = request.split('\r\n');
151
+ const requestLine = lines[0];
152
+ const headers = {};
153
+
154
+ // Parse headers
155
+ for (let i = 1; i < lines.length; i++) {
156
+ const line = lines[i];
157
+ if (line === '') break; // End of headers
158
+
159
+ const colonIndex = line.indexOf(':');
160
+ if (colonIndex > 0) {
161
+ const key = line.slice(0, colonIndex).trim().toLowerCase();
162
+ const value = line.slice(colonIndex + 1).trim();
163
+ headers[key] = value;
164
+ }
165
+ }
166
+
167
+ // Check if this is a WebSocket upgrade request
168
+ if (!requestLine.includes('GET') ||
169
+ headers['upgrade'] !== 'websocket' ||
170
+ !headers['sec-websocket-key']) {
171
+ // Not a WebSocket request
172
+ socket.write('HTTP/1.1 400 Bad Request\r\n\r\n');
173
+ socket.end();
174
+ return { success: false, bytesRead: buffer.length };
175
+ }
176
+
177
+ // Calculate accept key
178
+ const acceptKey = crypto
179
+ .createHash('sha1')
180
+ .update(headers['sec-websocket-key'] + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')
181
+ .digest('base64');
182
+
183
+ // Build response headers
184
+ const responseHeaders = [
185
+ 'HTTP/1.1 101 Switching Protocols',
186
+ 'Upgrade: websocket',
187
+ 'Connection: Upgrade',
188
+ `Sec-WebSocket-Accept: ${acceptKey}`
189
+ ];
190
+
191
+ // Add optional headers
192
+ if (headers['sec-websocket-protocol']) {
193
+ responseHeaders.push(`Sec-WebSocket-Protocol: ${headers['sec-websocket-protocol']}`);
194
+ }
195
+
196
+ if (this.config.compression && headers['sec-websocket-extensions']) {
197
+ responseHeaders.push('Sec-WebSocket-Extensions: permessage-deflate');
198
+ }
199
+
200
+ // Send handshake response
201
+ socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');
202
+
203
+ // Create connection object
204
+ const connection = {
205
+ id: connectionId,
206
+ socket: socket,
207
+ readyState: 1, // OPEN
208
+ protocol: headers['sec-websocket-protocol'] || null,
209
+ extensions: headers['sec-websocket-extensions'] || null,
210
+ remoteAddress: socket.remoteAddress,
211
+ remotePort: socket.remotePort,
212
+ headers: headers
213
+ };
214
+
215
+ this.connections.set(connectionId, connection);
216
+
217
+ return {
218
+ success: true,
219
+ bytesRead: buffer.length,
220
+ connection: connection
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Set up WebSocket connection after handshake
226
+ * @param {net.Socket} socket - TCP socket
227
+ * @param {string} connectionId - Connection ID
228
+ * @param {Object} handshakeResult - Handshake result
229
+ * @private
230
+ */
231
+ _setupWebSocketConnection(socket, connectionId, handshakeResult) {
232
+ const connection = handshakeResult.connection;
233
+
234
+ // Emit connection event
235
+ this.emit('connection', connection);
236
+
237
+ // Set up ping/pong heartbeat
238
+ if (this.config.pingInterval) {
239
+ const pingInterval = setInterval(() => {
240
+ if (connection.readyState === 1) {
241
+ this._sendPing(socket);
242
+ } else {
243
+ clearInterval(pingInterval);
244
+ }
245
+ }, this.config.pingInterval);
246
+
247
+ connection.pingInterval = pingInterval;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Handle WebSocket data frames
253
+ * @param {net.Socket} socket - TCP socket
254
+ * @param {Buffer} buffer - Data buffer
255
+ * @param {string} connectionId - Connection ID
256
+ * @private
257
+ */
258
+ _handleWebSocketData(socket, buffer, connectionId) {
259
+ const connection = this.connections.get(connectionId);
260
+ if (!connection) return;
261
+
262
+ try {
263
+ const frames = FrameEncoder.parseFrames(buffer, this.config.maxPayload);
264
+
265
+ for (const frame of frames) {
266
+ switch (frame.opcode) {
267
+ case 0x1: // Text frame
268
+ const text = frame.payload.toString('utf8');
269
+ this.emit('message', connection, text, false);
270
+ break;
271
+
272
+ case 0x2: // Binary frame
273
+ this.emit('message', connection, frame.payload, true);
274
+ break;
275
+
276
+ case 0x8: // Close frame
277
+ const code = frame.payload.length >= 2 ?
278
+ frame.payload.readUInt16BE(0) : 1000;
279
+ const reason = frame.payload.length > 2 ?
280
+ frame.payload.slice(2).toString('utf8') : '';
281
+ this._sendCloseFrame(socket, code, reason);
282
+ connection.readyState = 3; // CLOSED
283
+ this.emit('close', connection, code, reason);
284
+ socket.end();
285
+ break;
286
+
287
+ case 0x9: // Ping frame
288
+ this._sendPong(socket, frame.payload);
289
+ break;
290
+
291
+ case 0xA: // Pong frame
292
+ this.emit('pong', connection);
293
+ break;
294
+ }
295
+ }
296
+ } catch (error) {
297
+ this.emit('error', {
298
+ type: 'frame_parse_error',
299
+ message: 'Failed to parse WebSocket frame',
300
+ error,
301
+ connectionId
302
+ });
303
+ this._sendCloseFrame(socket, 1002, 'Protocol error');
304
+ socket.end();
305
+ }
306
+ }
307
+
308
+ /**
309
+ * Send ping frame
310
+ * @param {net.Socket} socket - TCP socket
311
+ * @private
312
+ */
313
+ _sendPing(socket) {
314
+ const pingFrame = Buffer.from([0x89, 0x00]); // FIN + Ping opcode
315
+ socket.write(pingFrame);
316
+ }
317
+
318
+ /**
319
+ * Send pong frame
320
+ * @param {net.Socket} socket - TCP socket
321
+ * @param {Buffer} payload - Ping payload to echo back
322
+ * @private
323
+ */
324
+ _sendPong(socket, payload) {
325
+ const pongFrame = FrameEncoder.createFrame(0xA, payload); // Pong opcode
326
+ socket.write(pongFrame);
327
+ }
328
+
329
+ /**
330
+ * Send close frame
331
+ * @param {net.Socket} socket - TCP socket
332
+ * @param {number} code - Close code
333
+ * @param {string} reason - Close reason
334
+ * @private
335
+ */
336
+ _sendCloseFrame(socket, code, reason) {
337
+ const payload = Buffer.alloc(2 + Buffer.byteLength(reason));
338
+ payload.writeUInt16BE(code, 0);
339
+ if (reason) {
340
+ payload.write(reason, 2, 'utf8');
341
+ }
342
+ const closeFrame = FrameEncoder.createFrame(0x8, payload); // Close opcode
343
+ socket.write(closeFrame);
344
+ }
345
+
346
+ /**
347
+ * Remove connection
348
+ * @param {string} connectionId - Connection ID
349
+ * @private
350
+ */
351
+ _removeConnection(connectionId) {
352
+ const connection = this.connections.get(connectionId);
353
+ if (connection) {
354
+ if (connection.pingInterval) {
355
+ clearInterval(connection.pingInterval);
356
+ }
357
+ this.connections.delete(connectionId);
358
+ }
359
+ }
360
+
361
+ /**
362
+ * Create a WebSocket client
363
+ * @param {string} url - WebSocket URL
364
+ * @param {Object} options - Client options
365
+ * @returns {Promise<Object>} Client connection
366
+ */
367
+ async createClient(url, options = {}) {
368
+ // Parse URL
369
+ const urlObj = new URL(url);
370
+ const host = urlObj.hostname;
371
+ const port = parseInt(urlObj.port) || (urlObj.protocol === 'wss:' ? 443 : 80);
372
+ const path = urlObj.pathname + urlObj.search;
373
+
374
+ return new Promise((resolve, reject) => {
375
+ const socket = net.createConnection({ host, port }, () => {
376
+ // Send WebSocket handshake
377
+ const key = crypto.randomBytes(16).toString('base64');
378
+ const handshake = [
379
+ `GET ${path} HTTP/1.1`,
380
+ `Host: ${host}:${port}`,
381
+ 'Upgrade: websocket',
382
+ 'Connection: Upgrade',
383
+ `Sec-WebSocket-Key: ${key}`,
384
+ 'Sec-WebSocket-Version: 13'
385
+ ].join('\r\n') + '\r\n\r\n';
386
+
387
+ socket.write(handshake);
388
+
389
+ let buffer = Buffer.alloc(0);
390
+
391
+ const onData = (chunk) => {
392
+ buffer = Buffer.concat([buffer, chunk]);
393
+
394
+ // Check for handshake response
395
+ const response = buffer.toString('utf8');
396
+ if (response.includes('\r\n\r\n')) {
397
+ socket.removeListener('data', onData);
398
+
399
+ if (response.includes('101 Switching Protocols')) {
400
+ const connection = {
401
+ id: crypto.randomUUID(),
402
+ socket: socket,
403
+ readyState: 1,
404
+ send: (data) => this._sendData(socket, data),
405
+ close: (code, reason) => {
406
+ this._sendCloseFrame(socket, code, reason);
407
+ socket.end();
408
+ }
409
+ };
410
+
411
+ // Set up message handling
412
+ socket.on('data', (data) => {
413
+ this._handleWebSocketData(socket, data, connection.id);
414
+ });
415
+
416
+ resolve(connection);
417
+ } else {
418
+ reject(new Error('Handshake failed'));
419
+ }
420
+ }
421
+ };
422
+
423
+ socket.on('data', onData);
424
+ });
425
+
426
+ socket.on('error', reject);
427
+ socket.on('timeout', () => reject(new Error('Connection timeout')));
428
+ });
429
+ }
430
+
431
+ /**
432
+ * Send data through socket
433
+ * @param {net.Socket} socket - TCP socket
434
+ * @param {string|Buffer} data - Data to send
435
+ * @private
436
+ */
437
+ _sendData(socket, data) {
438
+ const frame = FrameEncoder.createFrame(
439
+ 0x1, // Text frame opcode
440
+ Buffer.isBuffer(data) ? data : Buffer.from(data, 'utf8')
441
+ );
442
+ socket.write(frame);
443
+ }
444
+
445
+ /**
446
+ * Close the server
447
+ * @returns {Promise<void>}
448
+ */
449
+ async closeServer() {
450
+ return new Promise((resolve) => {
451
+ if (this.server) {
452
+ // Close all connections
453
+ this.connections.forEach((connection) => {
454
+ if (connection.readyState === 1) {
455
+ this._sendCloseFrame(connection.socket, 1001, 'Server going away');
456
+ connection.socket.end();
457
+ }
458
+ });
459
+
460
+ this.server.close(() => {
461
+ this.connections.clear();
462
+ resolve();
463
+ });
464
+ } else {
465
+ resolve();
466
+ }
467
+ });
468
+ }
469
+ }
470
+
471
+ export default TCPDriver;