@aetherframework/websocket 1.0.0

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,93 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/src/core/HandshakeHandler
6
+ */
7
+
8
+
9
+ import crypto from 'crypto';
10
+
11
+ class HandshakeHandler {
12
+ static GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
13
+
14
+ /**
15
+ * Parse HTTP headers from raw buffer
16
+ * @param {Buffer} buffer
17
+ * @returns {Object|null} Parsed request or null if incomplete
18
+ */
19
+ static parseRequest(buffer) {
20
+ const str = buffer.toString('utf8');
21
+ const endOfHeaders = str.indexOf('\r\n\r\n');
22
+
23
+ if (endOfHeaders === -1) return null;
24
+
25
+ const lines = str.slice(0, endOfHeaders).split('\r\n');
26
+ const requestLine = lines;
27
+ const headers = {};
28
+
29
+ // Parse method and path
30
+ const [method, path] = requestLine.split(' ');
31
+
32
+ // Parse headers
33
+ for (let i = 1; i < lines.length; i++) {
34
+ const index = lines[i].indexOf(':');
35
+ if (index > 0) {
36
+ const key = lines[i].slice(0, index).trim().toLowerCase();
37
+ const value = lines[i].slice(index + 1).trim();
38
+ headers[key] = value;
39
+ }
40
+ }
41
+
42
+ return {
43
+ method,
44
+ path,
45
+ headers,
46
+ rawHeaderSize: endOfHeaders + 4
47
+ };
48
+ }
49
+
50
+ /**
51
+ * Validate WebSocket upgrade request
52
+ * @param {Object} request
53
+ * @returns {boolean}
54
+ */
55
+ static isValidUpgrade(request) {
56
+ return (
57
+ request.method === 'GET' &&
58
+ request.headers['upgrade']?.toLowerCase() === 'websocket' &&
59
+ request.headers['connection']?.toLowerCase().includes('upgrade') &&
60
+ request.headers['sec-websocket-key'] &&
61
+ request.headers['sec-websocket-version'] === '13'
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Generate handshake response headers
67
+ * @param {string} key - Sec-WebSocket-Key from client
68
+ * @param {string} protocol - Optional subprotocol
69
+ * @returns {Buffer} Response headers
70
+ */
71
+ static createResponse(key, protocol = null) {
72
+ const accept = crypto
73
+ .createHash('sha1')
74
+ .update(key + this.GUID)
75
+ .digest('base64');
76
+
77
+ let response = [
78
+ 'HTTP/1.1 101 Switching Protocols',
79
+ 'Upgrade: websocket',
80
+ 'Connection: Upgrade',
81
+ `Sec-WebSocket-Accept: ${accept}`
82
+ ];
83
+
84
+ if (protocol) {
85
+ response.push(`Sec-WebSocket-Protocol: ${protocol}`);
86
+ }
87
+
88
+ response.push('', ''); // Empty line to end headers
89
+ return Buffer.from(response.join('\r\n'), 'utf8');
90
+ }
91
+ }
92
+
93
+ export default HandshakeHandler;
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/src/core/ProtocolHandler
6
+ */
7
+
8
+
9
+ class ProtocolHandler {
10
+ constructor(options = {}) {
11
+ this.options = {
12
+ supportedProtocols: options.supportedProtocols || [],
13
+ maxMessageSize: options.maxMessageSize || 1024 * 1024, // 1MB
14
+ autoPing: options.autoPing !== false,
15
+ pingInterval: options.pingInterval || 30000,
16
+ pingTimeout: options.pingTimeout || 10000,
17
+ ...options
18
+ };
19
+
20
+ this.messageHandlers = new Map();
21
+ this.protocols = new Set(this.options.supportedProtocols);
22
+ }
23
+
24
+ /**
25
+ * Handle WebSocket handshake protocol negotiation
26
+ * @param {Object} request - HTTP request object
27
+ * @returns {string|null} Selected protocol or null
28
+ */
29
+ negotiateProtocol(request) {
30
+ const clientProtocols = request.headers['sec-websocket-protocol'];
31
+ if (!clientProtocols) return null;
32
+
33
+ const requestedProtocols = clientProtocols.split(',').map(p => p.trim());
34
+
35
+ // Find first matching protocol
36
+ for (const protocol of requestedProtocols) {
37
+ if (this.protocols.has(protocol)) {
38
+ return protocol;
39
+ }
40
+ }
41
+
42
+ return null;
43
+ }
44
+
45
+ /**
46
+ * Register message handler for specific protocol
47
+ * @param {string} protocol - Protocol name
48
+ * @param {Function} handler - Message handler function
49
+ */
50
+ registerHandler(protocol, handler) {
51
+ this.messageHandlers.set(protocol, handler);
52
+ }
53
+
54
+ /**
55
+ * Process incoming WebSocket message
56
+ * @param {Object} connection - WebSocket connection
57
+ * @param {Buffer|string} message - Raw message
58
+ * @param {boolean} isBinary - Whether message is binary
59
+ * @returns {Promise<any>} Processing result
60
+ */
61
+ async processMessage(connection, message, isBinary = false) {
62
+ const protocol = connection.protocol;
63
+ const handler = this.messageHandlers.get(protocol) || this.messageHandlers.get('*');
64
+
65
+ if (!handler) {
66
+ throw new Error(`No handler registered for protocol: ${protocol}`);
67
+ }
68
+
69
+ // Validate message size
70
+ const messageSize = isBinary ? message.length : Buffer.byteLength(message);
71
+ if (messageSize > this.options.maxMessageSize) {
72
+ throw new Error(`Message size ${messageSize} exceeds maximum ${this.options.maxMessageSize}`);
73
+ }
74
+
75
+ // Parse message based on protocol
76
+ let parsedMessage;
77
+ if (protocol === 'json') {
78
+ try {
79
+ parsedMessage = JSON.parse(message.toString());
80
+ } catch (error) {
81
+ throw new Error(`Invalid JSON: ${error.message}`);
82
+ }
83
+ } else if (protocol === 'binary') {
84
+ parsedMessage = message;
85
+ } else {
86
+ parsedMessage = message.toString();
87
+ }
88
+
89
+ // Execute handler
90
+ return await handler(connection, parsedMessage, isBinary);
91
+ }
92
+
93
+ /**
94
+ * Handle ping/pong heartbeat
95
+ * @param {Object} connection - WebSocket connection
96
+ * @param {Buffer} data - Ping data
97
+ * @returns {Buffer} Pong response
98
+ */
99
+ handlePing(connection, data) {
100
+ connection.lastPingTime = Date.now();
101
+ return data; // Echo back the same data for pong
102
+ }
103
+
104
+ /**
105
+ * Handle pong response
106
+ * @param {Object} connection - WebSocket connection
107
+ * @param {Buffer} data - Pong data
108
+ */
109
+ handlePong(connection, data) {
110
+ connection.lastPongTime = Date.now();
111
+ connection.isAlive = true;
112
+ }
113
+
114
+ /**
115
+ * Handle close frame
116
+ * @param {Object} connection - WebSocket connection
117
+ * @param {number} code - Close code
118
+ * @param {string} reason - Close reason
119
+ * @returns {Object} Close response
120
+ */
121
+ handleClose(connection, code, reason) {
122
+ const validCodes = [
123
+ 1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011,
124
+ 3000, 3999, 4000, 4999
125
+ ];
126
+
127
+ if (!validCodes.includes(code)) {
128
+ code = 1002; // Protocol error
129
+ }
130
+
131
+ return {
132
+ code,
133
+ reason: reason || this.getCloseReason(code)
134
+ };
135
+ }
136
+
137
+ /**
138
+ * Get human-readable close reason
139
+ * @param {number} code - Close code
140
+ * @returns {string} Reason description
141
+ */
142
+ getCloseReason(code) {
143
+ const reasons = {
144
+ 1000: 'Normal closure',
145
+ 1001: 'Going away',
146
+ 1002: 'Protocol error',
147
+ 1003: 'Unsupported data',
148
+ 1007: 'Invalid frame payload data',
149
+ 1008: 'Policy violation',
150
+ 1009: 'Message too big',
151
+ 1010: 'Mandatory extension',
152
+ 1011: 'Internal server error',
153
+ 3000: 'Custom code start',
154
+ 3999: 'Custom code end',
155
+ 4000: 'Reserved'
156
+ };
157
+
158
+ return reasons[code] || 'Unknown reason';
159
+ }
160
+
161
+ /**
162
+ * Validate WebSocket version
163
+ * @param {string} version - WebSocket version
164
+ * @returns {boolean} Whether version is supported
165
+ */
166
+ validateVersion(version) {
167
+ return version === '13'; // RFC 6455
168
+ }
169
+
170
+ /**
171
+ * Get protocol statistics
172
+ * @returns {Object} Statistics object
173
+ */
174
+ getStats() {
175
+ return {
176
+ supportedProtocols: Array.from(this.protocols),
177
+ registeredHandlers: this.messageHandlers.size,
178
+ maxMessageSize: this.options.maxMessageSize,
179
+ autoPing: this.options.autoPing,
180
+ pingInterval: this.options.pingInterval,
181
+ pingTimeout: this.options.pingTimeout
182
+ };
183
+ }
184
+ }
185
+
186
+ export default ProtocolHandler;
@@ -0,0 +1,293 @@
1
+ /**
2
+ * @license MIT
3
+ * Copyright (c) 2026-present AetherFramework Contributors.
4
+ * SPDX-License-Identifier: MIT
5
+ * @module @aetherframework/src/core/WebSocketFactory
6
+ */
7
+
8
+ import EventEmitter from 'events';
9
+ import ConnectionManager from './ConnectionManager.js';
10
+ import FrameParser from './FrameParser.js';
11
+ import ProtocolHandler from './ProtocolHandler.js';
12
+ import ConfigLoader from '../utils/config-loader.js';
13
+
14
+ class WebSocketFactory extends EventEmitter {
15
+ /**
16
+ * Create a new WebSocketFactory instance
17
+ * @param {Object} options - Configuration options
18
+ */
19
+ constructor(options = {}) {
20
+ super();
21
+
22
+ // Load configuration from environment and options
23
+ this.config = ConfigLoader.load({
24
+ // Default configuration
25
+ driver: 'tcp',
26
+ port: 8080,
27
+ host: '0.0.0.0',
28
+ maxPayload: 1024 * 1024, // 1MB
29
+ pingInterval: 30000, // 30 seconds
30
+ pingTimeout: 10000, // 10 seconds
31
+ compression: false,
32
+ tls: false,
33
+ tlsOptions: {},
34
+ // Merge with user options
35
+ ...options
36
+ });
37
+
38
+ this.connectionManager = new ConnectionManager();
39
+ this.frameParser = new FrameParser(this.config);
40
+ this.protocolHandler = new ProtocolHandler(this.config);
41
+ this.driver = null;
42
+ this.server = null;
43
+ this.middleware = [];
44
+
45
+ // Initialize driver
46
+ this._initializeDriver();
47
+ }
48
+
49
+ /**
50
+ * Initialize the selected transport driver
51
+ * @private
52
+ */
53
+ async _initializeDriver() {
54
+ try {
55
+ const driverName = this.config.driver || 'tcp';
56
+ const driverModule = await import(`../drivers/${driverName}-driver.js`);
57
+ this.driver = new driverModule.default(this.config);
58
+
59
+ // Set up driver event forwarding
60
+ this._setupDriverEvents();
61
+
62
+ this.emit('driver:initialized', { driver: driverName });
63
+ } catch (error) {
64
+ this.emit('error', {
65
+ type: 'driver_initialization',
66
+ message: `Failed to initialize driver: ${error.message}`,
67
+ error
68
+ });
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Set up event forwarding from driver to factory
75
+ * @private
76
+ */
77
+ _setupDriverEvents() {
78
+ if (!this.driver) return;
79
+
80
+ // Forward all driver events to factory
81
+ const eventsToForward = [
82
+ 'server:listening',
83
+ 'server:closed',
84
+ 'server:error',
85
+ 'connection',
86
+ 'message',
87
+ 'close',
88
+ 'error',
89
+ 'ping',
90
+ 'pong'
91
+ ];
92
+
93
+ eventsToForward.forEach(event => {
94
+ this.driver.on(event, (...args) => {
95
+ this.emit(event, ...args);
96
+ });
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Create a WebSocket server
102
+ * @param {Object} options - Server-specific options
103
+ * @returns {Promise<Object>} Server instance
104
+ */
105
+ async createServer(options = {}) {
106
+ if (!this.driver) {
107
+ await this._initializeDriver();
108
+ }
109
+
110
+ const serverOptions = {
111
+ ...this.config,
112
+ ...options
113
+ };
114
+
115
+ try {
116
+ this.server = await this.driver.createServer(serverOptions);
117
+
118
+ // CRITICAL FIX: Ensure server:listening event is emitted
119
+ // Get server address information
120
+ let addressInfo;
121
+ if (this.server.address && typeof this.server.address === 'function') {
122
+ addressInfo = this.server.address();
123
+ } else if (this.server.address) {
124
+ addressInfo = this.server.address;
125
+ } else {
126
+ addressInfo = {
127
+ address: serverOptions.host || '0.0.0.0',
128
+ port: serverOptions.port || 8080,
129
+ family: 'IPv4'
130
+ };
131
+ }
132
+
133
+ // Emit server:listening event with address information
134
+ // Use setTimeout to ensure event listeners are registered
135
+ setTimeout(() => {
136
+ this.emit('server:listening', {
137
+ host: addressInfo.address,
138
+ port: addressInfo.port,
139
+ family: addressInfo.family || 'IPv4'
140
+ });
141
+ }, 0);
142
+
143
+ this.emit('server:created', this.server);
144
+ return this.server;
145
+ } catch (error) {
146
+ this.emit('error', {
147
+ type: 'server_creation',
148
+ message: `Failed to create server: ${error.message}`,
149
+ error
150
+ });
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Create a WebSocket client connection
157
+ * @param {string} url - WebSocket URL (ws:// or wss://)
158
+ * @param {Object} options - Client options
159
+ * @returns {Promise<Object>} Client connection
160
+ */
161
+ async createClient(url, options = {}) {
162
+ if (!this.driver) {
163
+ await this._initializeDriver();
164
+ }
165
+
166
+ const clientOptions = {
167
+ ...this.config,
168
+ ...options
169
+ };
170
+
171
+ try {
172
+ const client = await this.driver.createClient(url, clientOptions);
173
+ this.emit('client:created', client);
174
+ return client;
175
+ } catch (error) {
176
+ this.emit('error', {
177
+ type: 'client_creation',
178
+ message: `Failed to create client: ${error.message}`,
179
+ error
180
+ });
181
+ throw error;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * Add middleware to the WebSocket pipeline
187
+ * @param {Function} middleware - Middleware function
188
+ */
189
+ use(middleware) {
190
+ this.middleware.push(middleware);
191
+ }
192
+
193
+ /**
194
+ * Broadcast message to all connected clients
195
+ * @param {string|Buffer} message - Message to broadcast
196
+ * @param {Function} filter - Optional filter function
197
+ */
198
+ broadcast(message, filter = null) {
199
+ const connections = this.connectionManager.getAll();
200
+
201
+ connections.forEach(connection => {
202
+ if (!filter || filter(connection)) {
203
+ try {
204
+ connection.send(message);
205
+ } catch (error) {
206
+ this.emit('error', {
207
+ type: 'broadcast',
208
+ message: `Failed to broadcast to connection ${connection.id}`,
209
+ error,
210
+ connection
211
+ });
212
+ }
213
+ }
214
+ });
215
+ }
216
+
217
+ /**
218
+ * Send message to specific connection
219
+ * @param {string} connectionId - Connection ID
220
+ * @param {string|Buffer} message - Message to send
221
+ * @returns {boolean} Success status
222
+ */
223
+ sendTo(connectionId, message) {
224
+ const connection = this.connectionManager.get(connectionId);
225
+ if (connection && connection.readyState === 1) { // OPEN
226
+ return connection.send(message);
227
+ }
228
+ return false;
229
+ }
230
+
231
+ /**
232
+ * Get connection by ID
233
+ * @param {string} connectionId - Connection ID
234
+ * @returns {Object|null} Connection object or null
235
+ */
236
+ getConnection(connectionId) {
237
+ return this.connectionManager.get(connectionId);
238
+ }
239
+
240
+ /**
241
+ * Get all connections
242
+ * @returns {Array} Array of connection objects
243
+ */
244
+ getConnections() {
245
+ return this.connectionManager.getAll();
246
+ }
247
+
248
+ /**
249
+ * Get connection statistics
250
+ * @returns {Object} Statistics object
251
+ */
252
+ getStats() {
253
+ const connections = this.connectionManager.getAll();
254
+
255
+ return {
256
+ totalConnections: connections.length,
257
+ activeConnections: connections.filter(c => c.readyState === 1).length,
258
+ driver: this.config.driver,
259
+ uptime: this.server ? Date.now() - this.server.startTime : 0,
260
+ memoryUsage: process.memoryUsage(),
261
+ config: {
262
+ maxPayload: this.config.maxPayload,
263
+ pingInterval: this.config.pingInterval,
264
+ pingTimeout: this.config.pingTimeout,
265
+ compression: this.config.compression
266
+ }
267
+ };
268
+ }
269
+
270
+ /**
271
+ * Close the WebSocket server
272
+ * @returns {Promise<void>}
273
+ */
274
+ async close() {
275
+ if (this.server) {
276
+ await this.driver.closeServer(this.server);
277
+ this.emit('server:closed');
278
+ }
279
+
280
+ // Close all connections
281
+ const connections = this.connectionManager.getAll();
282
+ connections.forEach(connection => {
283
+ if (connection.readyState === 1) {
284
+ connection.close(1000, 'Server shutting down');
285
+ }
286
+ });
287
+
288
+ this.connectionManager.clear();
289
+ this.emit('closed');
290
+ }
291
+ }
292
+
293
+ export default WebSocketFactory;