@aetherframework/websocket 1.0.0 → 1.0.1
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/LICENSE +21 -0
- package/package.json +1 -2
- package/src/core/ConnectionManager.js +0 -213
- package/src/core/FrameParser.js +0 -115
- package/src/core/HandshakeHandler.js +0 -93
- package/src/core/ProtocolHandler.js +0 -186
- package/src/core/WebSocketFactory.js +0 -293
- package/src/drivers/http-driver.js +0 -576
- package/src/drivers/index.js +0 -29
- package/src/drivers/memory-driver.js +0 -422
- package/src/drivers/tcp-driver.js +0 -471
- package/src/drivers/tls-driver.js +0 -502
- package/src/middleware/auth-middleware.js +0 -37
- package/src/middleware/broadcast-manager.js +0 -173
- package/src/middleware/compression.js +0 -194
- package/src/middleware/message-logger.js +0 -322
- package/src/middleware/rate-limiter.js +0 -142
- package/src/utils/config-loader.js +0 -183
- package/src/utils/connection-pool.js +0 -110
- package/src/utils/error-handler.js +0 -59
- package/src/utils/frame-encoder.js +0 -211
- package/src/utils/heartbeat-manager.js +0 -91
|
@@ -1,471 +0,0 @@
|
|
|
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;
|