@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,576 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/drivers/http-driver
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
import http from "http";
|
|
10
|
+
import crypto from "crypto";
|
|
11
|
+
import EventEmitter from "events";
|
|
12
|
+
import FrameEncoder from "../utils/frame-encoder.js"; // For encoding WebSocket frames
|
|
13
|
+
import FrameParser from "../core/FrameParser.js"; // For parsing WebSocket frames
|
|
14
|
+
import HandshakeHandler from "../core/HandshakeHandler.js";
|
|
15
|
+
|
|
16
|
+
class HTTPDriver extends EventEmitter {
|
|
17
|
+
/**
|
|
18
|
+
* Constructor for HTTPDriver
|
|
19
|
+
* @param {Object} config - Configuration object
|
|
20
|
+
* @param {number} config.port - Server port (default: 80)
|
|
21
|
+
* @param {string} config.host - Server host (default: "0.0.0.0")
|
|
22
|
+
* @param {number} config.maxPayload - Maximum payload size in bytes (default: 1MB)
|
|
23
|
+
* @param {number} config.pingInterval - Ping interval in milliseconds
|
|
24
|
+
* @param {number} config.socketTimeout - Socket timeout in milliseconds
|
|
25
|
+
* @param {boolean} config.compression - Enable WebSocket compression
|
|
26
|
+
*/
|
|
27
|
+
constructor(config = {}) {
|
|
28
|
+
super();
|
|
29
|
+
this.config = {
|
|
30
|
+
port: 80,
|
|
31
|
+
host: "0.0.0.0",
|
|
32
|
+
maxPayload: config.maxPayload || 1024 * 1024,
|
|
33
|
+
...config,
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
this.server = null;
|
|
37
|
+
this.connections = new Map(); // Map of connectionId -> connection object
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Create an HTTP WebSocket server
|
|
42
|
+
* @param {Object} options - Server options
|
|
43
|
+
* @returns {Promise<Object>} Server instance with type, address, and close method
|
|
44
|
+
*/
|
|
45
|
+
async createServer(options = {}) {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
try {
|
|
48
|
+
// Create HTTP server
|
|
49
|
+
this.server = http.createServer((req, res) => {
|
|
50
|
+
// Handle regular HTTP requests
|
|
51
|
+
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
52
|
+
res.end("WebSocket server is running");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Handle WebSocket upgrade requests
|
|
56
|
+
this.server.on("upgrade", (req, socket, head) => {
|
|
57
|
+
this._handleUpgrade(req, socket, head);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const serverOptions = {
|
|
61
|
+
host: options.host || this.config.host,
|
|
62
|
+
port: options.port || this.config.port,
|
|
63
|
+
...options,
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Start listening on specified port and host
|
|
67
|
+
this.server.listen(serverOptions, () => {
|
|
68
|
+
const address = this.server.address();
|
|
69
|
+
|
|
70
|
+
// Emit server:listening event with server details
|
|
71
|
+
this.emit("server:listening", {
|
|
72
|
+
host: address.address,
|
|
73
|
+
port: address.port,
|
|
74
|
+
family: address.family,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
resolve({
|
|
78
|
+
type: "http",
|
|
79
|
+
address: address,
|
|
80
|
+
close: () => this.closeServer(),
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Handle server errors
|
|
85
|
+
this.server.on("error", (error) => {
|
|
86
|
+
this.emit("error", {
|
|
87
|
+
type: "server_error",
|
|
88
|
+
message: "HTTP server error",
|
|
89
|
+
error,
|
|
90
|
+
});
|
|
91
|
+
reject(error);
|
|
92
|
+
});
|
|
93
|
+
} catch (error) {
|
|
94
|
+
reject(error);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Handle HTTP upgrade to WebSocket
|
|
101
|
+
* @param {http.IncomingMessage} req - HTTP request object
|
|
102
|
+
* @param {net.Socket} socket - Raw TCP socket
|
|
103
|
+
* @param {Buffer} head - First packet of the upgraded stream
|
|
104
|
+
* @private
|
|
105
|
+
*/
|
|
106
|
+
_handleUpgrade(req, socket, head) {
|
|
107
|
+
// Validate WebSocket upgrade request
|
|
108
|
+
if (!this._isValidWebSocketUpgrade(req)) {
|
|
109
|
+
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
|
110
|
+
socket.destroy();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Perform WebSocket handshake
|
|
115
|
+
const key = req.headers["sec-websocket-key"];
|
|
116
|
+
const acceptKey = crypto
|
|
117
|
+
.createHash("sha1")
|
|
118
|
+
.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11")
|
|
119
|
+
.digest("base64");
|
|
120
|
+
|
|
121
|
+
const headers = [
|
|
122
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
123
|
+
"Upgrade: websocket",
|
|
124
|
+
"Connection: Upgrade",
|
|
125
|
+
`Sec-WebSocket-Accept: ${acceptKey}`,
|
|
126
|
+
];
|
|
127
|
+
|
|
128
|
+
// Add optional protocol header if requested
|
|
129
|
+
if (req.headers["sec-websocket-protocol"]) {
|
|
130
|
+
const protocol = req.headers["sec-websocket-protocol"];
|
|
131
|
+
headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Add optional extensions header if compression is enabled
|
|
135
|
+
if (this.config.compression && req.headers["sec-websocket-extensions"]) {
|
|
136
|
+
headers.push("Sec-WebSocket-Extensions: permessage-deflate");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const response = headers.join("\r\n") + "\r\n\r\n";
|
|
140
|
+
socket.write(response);
|
|
141
|
+
|
|
142
|
+
// Create connection object
|
|
143
|
+
const connectionId = crypto.randomUUID();
|
|
144
|
+
const connection = {
|
|
145
|
+
id: connectionId,
|
|
146
|
+
socket: socket,
|
|
147
|
+
req: req,
|
|
148
|
+
readyState: 1, // OPEN state
|
|
149
|
+
protocol: req.headers["sec-websocket-protocol"] || null,
|
|
150
|
+
extensions: req.headers["sec-websocket-extensions"] || null,
|
|
151
|
+
remoteAddress: req.socket.remoteAddress,
|
|
152
|
+
remotePort: req.socket.remotePort,
|
|
153
|
+
headers: req.headers,
|
|
154
|
+
url: req.url,
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
this.connections.set(connectionId, connection);
|
|
158
|
+
|
|
159
|
+
// Configure socket options
|
|
160
|
+
socket.setNoDelay(true);
|
|
161
|
+
socket.setKeepAlive(true, 10000);
|
|
162
|
+
socket.setTimeout(this.config.socketTimeout || 30000);
|
|
163
|
+
|
|
164
|
+
// Process any buffered data from the upgrade
|
|
165
|
+
if (head && head.length > 0) {
|
|
166
|
+
this._handleWebSocketData(socket, head);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Set up socket event handlers
|
|
170
|
+
socket.on("data", (data) => {
|
|
171
|
+
this._handleWebSocketData(socket, data);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
socket.on("close", () => {
|
|
175
|
+
this.emit("close", connection, 1006, "Connection closed");
|
|
176
|
+
this._removeConnection(connectionId);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
socket.on("error", (error) => {
|
|
180
|
+
this.emit("error", {
|
|
181
|
+
type: "socket_error",
|
|
182
|
+
message: "HTTP socket error",
|
|
183
|
+
error,
|
|
184
|
+
connectionId,
|
|
185
|
+
});
|
|
186
|
+
this._removeConnection(connectionId);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
socket.on("timeout", () => {
|
|
190
|
+
socket.end();
|
|
191
|
+
this._removeConnection(connectionId);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Emit connection event
|
|
195
|
+
this.emit("connection", connection);
|
|
196
|
+
|
|
197
|
+
// Set up heartbeat/ping interval if configured
|
|
198
|
+
if (this.config.pingInterval) {
|
|
199
|
+
const pingInterval = setInterval(() => {
|
|
200
|
+
if (connection.readyState === 1) {
|
|
201
|
+
this._sendPing(socket);
|
|
202
|
+
} else {
|
|
203
|
+
clearInterval(pingInterval);
|
|
204
|
+
}
|
|
205
|
+
}, this.config.pingInterval);
|
|
206
|
+
|
|
207
|
+
connection.pingInterval = pingInterval;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate WebSocket upgrade request
|
|
213
|
+
* @param {http.IncomingMessage} req - HTTP request
|
|
214
|
+
* @returns {boolean} Whether request is a valid WebSocket upgrade
|
|
215
|
+
* @private
|
|
216
|
+
*/
|
|
217
|
+
_isValidWebSocketUpgrade(req) {
|
|
218
|
+
const upgrade = req.headers.upgrade;
|
|
219
|
+
const connection = req.headers.connection;
|
|
220
|
+
const key = req.headers["sec-websocket-key"];
|
|
221
|
+
const version = req.headers["sec-websocket-version"];
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
upgrade &&
|
|
225
|
+
upgrade.toLowerCase() === "websocket" &&
|
|
226
|
+
connection &&
|
|
227
|
+
connection.toLowerCase().includes("upgrade") &&
|
|
228
|
+
key &&
|
|
229
|
+
key.length === 24 &&
|
|
230
|
+
version === "13"
|
|
231
|
+
);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Handle WebSocket data frames
|
|
236
|
+
* @param {net.Socket} socket - Raw socket
|
|
237
|
+
* @param {Buffer} data - Data buffer containing WebSocket frames
|
|
238
|
+
* @private
|
|
239
|
+
*/
|
|
240
|
+
_handleWebSocketData(socket, data) {
|
|
241
|
+
const connection = this.connections.get(socket);
|
|
242
|
+
const connectionId = connection?.id || 'unknown';
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
// Parse WebSocket frames from the data buffer
|
|
246
|
+
const result = FrameParser.parse(data, this.config.maxPayload);
|
|
247
|
+
|
|
248
|
+
// Process all parsed frames
|
|
249
|
+
for (const frame of result.frames) {
|
|
250
|
+
this._processWebSocketFrame(socket, frame, connection);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
} catch (error) {
|
|
254
|
+
console.error(`Frame parsing error for connection ${connectionId}:`, error);
|
|
255
|
+
|
|
256
|
+
this.emit("error", {
|
|
257
|
+
type: "frame_parse_error",
|
|
258
|
+
message: "Failed to parse WebSocket frame",
|
|
259
|
+
error,
|
|
260
|
+
connectionId,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// Send close frame and close connection on protocol error
|
|
264
|
+
this._sendCloseFrame(socket, 1002, "Protocol error");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Process a single WebSocket frame
|
|
270
|
+
* @param {net.Socket} socket - Raw socket
|
|
271
|
+
* @param {Object} frame - Parsed frame object
|
|
272
|
+
* @param {Object} connection - Connection object
|
|
273
|
+
* @private
|
|
274
|
+
*/
|
|
275
|
+
_processWebSocketFrame(socket, frame, connection) {
|
|
276
|
+
if (!connection) return;
|
|
277
|
+
|
|
278
|
+
switch (frame.opcode) {
|
|
279
|
+
case 0x1: // TEXT frame
|
|
280
|
+
this.emit(
|
|
281
|
+
"message",
|
|
282
|
+
connection,
|
|
283
|
+
frame.payload.toString("utf8"),
|
|
284
|
+
false,
|
|
285
|
+
);
|
|
286
|
+
break;
|
|
287
|
+
case 0x2: // BINARY frame
|
|
288
|
+
this.emit(
|
|
289
|
+
"message",
|
|
290
|
+
connection,
|
|
291
|
+
frame.payload,
|
|
292
|
+
true,
|
|
293
|
+
);
|
|
294
|
+
break;
|
|
295
|
+
case 0x8: // CLOSE frame
|
|
296
|
+
this._handleCloseFrame(socket, frame);
|
|
297
|
+
break;
|
|
298
|
+
case 0x9: // PING frame
|
|
299
|
+
this._handlePingFrame(socket, frame);
|
|
300
|
+
break;
|
|
301
|
+
case 0xa: // PONG frame
|
|
302
|
+
this._handlePongFrame(socket, frame);
|
|
303
|
+
break;
|
|
304
|
+
case 0x0: // CONTINUATION frame
|
|
305
|
+
this._handleContinuationFrame(socket, frame);
|
|
306
|
+
break;
|
|
307
|
+
default:
|
|
308
|
+
console.warn(`Unknown WebSocket opcode: 0x${frame.opcode.toString(16)}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Handle Close frame
|
|
314
|
+
* @param {net.Socket} socket - Raw socket
|
|
315
|
+
* @param {Object} frame - Parsed frame object
|
|
316
|
+
* @private
|
|
317
|
+
*/
|
|
318
|
+
_handleCloseFrame(socket, frame) {
|
|
319
|
+
const connection = this.connections.get(socket);
|
|
320
|
+
if (!connection) return;
|
|
321
|
+
|
|
322
|
+
let code = 1000; // Default close code
|
|
323
|
+
let reason = "";
|
|
324
|
+
|
|
325
|
+
// Parse close code and reason from payload
|
|
326
|
+
if (frame.payload && frame.payload.length >= 2) {
|
|
327
|
+
code = frame.payload.readUInt16BE(0);
|
|
328
|
+
if (frame.payload.length > 2) {
|
|
329
|
+
reason = frame.payload.slice(2).toString("utf8");
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Send close frame response
|
|
334
|
+
this._sendCloseFrame(socket, code, reason);
|
|
335
|
+
|
|
336
|
+
// Update connection state
|
|
337
|
+
connection.readyState = 3; // CLOSED
|
|
338
|
+
|
|
339
|
+
// Emit close event
|
|
340
|
+
this.emit("close", connection, code, reason);
|
|
341
|
+
|
|
342
|
+
// Close socket
|
|
343
|
+
socket.end();
|
|
344
|
+
|
|
345
|
+
// Remove connection from active connections
|
|
346
|
+
this._removeConnection(connection.id);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Handle Ping frame
|
|
351
|
+
* @param {net.Socket} socket - Raw socket
|
|
352
|
+
* @param {Object} frame - Parsed frame object
|
|
353
|
+
* @private
|
|
354
|
+
*/
|
|
355
|
+
_handlePingFrame(socket, frame) {
|
|
356
|
+
// Echo back the ping payload as pong (RFC 6455 requirement)
|
|
357
|
+
this._sendPong(socket, frame.payload);
|
|
358
|
+
|
|
359
|
+
// Emit ping event for monitoring
|
|
360
|
+
const connection = this.connections.get(socket);
|
|
361
|
+
if (connection) {
|
|
362
|
+
this.emit("ping", connection, frame.payload);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Handle Pong frame
|
|
368
|
+
* @param {net.Socket} socket - Raw socket
|
|
369
|
+
* @param {Object} frame - Parsed frame object
|
|
370
|
+
* @private
|
|
371
|
+
*/
|
|
372
|
+
_handlePongFrame(socket, frame) {
|
|
373
|
+
// Emit pong event for monitoring
|
|
374
|
+
const connection = this.connections.get(socket);
|
|
375
|
+
if (connection) {
|
|
376
|
+
this.emit("pong", connection, frame.payload);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Handle Continuation frame (for fragmented messages)
|
|
382
|
+
* @param {net.Socket} socket - Raw socket
|
|
383
|
+
* @param {Object} frame - Parsed frame object
|
|
384
|
+
* @private
|
|
385
|
+
*/
|
|
386
|
+
_handleContinuationFrame(socket, frame) {
|
|
387
|
+
const connection = this.connections.get(socket);
|
|
388
|
+
if (!connection) return;
|
|
389
|
+
|
|
390
|
+
// Store continuation frame data
|
|
391
|
+
if (!connection.fragmentedData) {
|
|
392
|
+
connection.fragmentedData = [];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
connection.fragmentedData.push(frame.payload);
|
|
396
|
+
|
|
397
|
+
// If this is the final fragment, combine and emit
|
|
398
|
+
if (frame.fin) {
|
|
399
|
+
const completeData = Buffer.concat(connection.fragmentedData);
|
|
400
|
+
const isBinary = connection.fragmentedOpcode === 0x2;
|
|
401
|
+
|
|
402
|
+
this.emit(
|
|
403
|
+
"message",
|
|
404
|
+
connection,
|
|
405
|
+
isBinary ? completeData : completeData.toString("utf8"),
|
|
406
|
+
isBinary,
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
// Clean up fragmented data storage
|
|
410
|
+
delete connection.fragmentedData;
|
|
411
|
+
delete connection.fragmentedOpcode;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Send ping frame to client
|
|
417
|
+
* @param {net.Socket} socket - Raw socket
|
|
418
|
+
* @private
|
|
419
|
+
*/
|
|
420
|
+
_sendPing(socket) {
|
|
421
|
+
const pingFrame = FrameEncoder.createPingFrame();
|
|
422
|
+
socket.write(pingFrame);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Send pong frame to client
|
|
427
|
+
* @param {net.Socket} socket - Raw socket
|
|
428
|
+
* @param {Buffer} payload - Ping payload to echo back
|
|
429
|
+
* @private
|
|
430
|
+
*/
|
|
431
|
+
_sendPong(socket, payload) {
|
|
432
|
+
const pongFrame = FrameEncoder.createPongFrame(payload);
|
|
433
|
+
socket.write(pongFrame);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Send close frame to client
|
|
438
|
+
* @param {net.Socket} socket - Raw socket
|
|
439
|
+
* @param {number} code - Close status code
|
|
440
|
+
* @param {string} reason - Close reason
|
|
441
|
+
* @private
|
|
442
|
+
*/
|
|
443
|
+
_sendCloseFrame(socket, code, reason) {
|
|
444
|
+
const closeFrame = FrameEncoder.createCloseFrame(code, reason);
|
|
445
|
+
socket.write(closeFrame);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Remove connection from active connections
|
|
450
|
+
* @param {string} connectionId - Connection ID
|
|
451
|
+
* @private
|
|
452
|
+
*/
|
|
453
|
+
_removeConnection(connectionId) {
|
|
454
|
+
const connection = this.connections.get(connectionId);
|
|
455
|
+
if (connection) {
|
|
456
|
+
// Clear ping interval if exists
|
|
457
|
+
if (connection.pingInterval) {
|
|
458
|
+
clearInterval(connection.pingInterval);
|
|
459
|
+
}
|
|
460
|
+
this.connections.delete(connectionId);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Create a WebSocket client over HTTP
|
|
466
|
+
* @param {string} url - WebSocket URL (ws://)
|
|
467
|
+
* @param {Object} options - Client options
|
|
468
|
+
* @returns {Promise<Object>} Client connection object
|
|
469
|
+
*/
|
|
470
|
+
async createClient(url, options = {}) {
|
|
471
|
+
const urlObj = new URL(url);
|
|
472
|
+
const host = urlObj.hostname;
|
|
473
|
+
const port = parseInt(urlObj.port) || 80;
|
|
474
|
+
const path = urlObj.pathname + urlObj.search;
|
|
475
|
+
|
|
476
|
+
return new Promise((resolve, reject) => {
|
|
477
|
+
const req = http.request({
|
|
478
|
+
hostname: host,
|
|
479
|
+
port: port,
|
|
480
|
+
path: path,
|
|
481
|
+
method: "GET",
|
|
482
|
+
headers: {
|
|
483
|
+
Upgrade: "websocket",
|
|
484
|
+
Connection: "Upgrade",
|
|
485
|
+
"Sec-WebSocket-Key": crypto.randomBytes(16).toString("base64"),
|
|
486
|
+
"Sec-WebSocket-Version": "13",
|
|
487
|
+
...options.headers,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
req.on("upgrade", (res, socket, head) => {
|
|
492
|
+
const connection = {
|
|
493
|
+
id: crypto.randomUUID(),
|
|
494
|
+
socket: socket,
|
|
495
|
+
readyState: 1,
|
|
496
|
+
send: (data) => this._sendData(socket, data),
|
|
497
|
+
close: (code, reason) => {
|
|
498
|
+
this._sendCloseFrame(socket, code, reason);
|
|
499
|
+
socket.end();
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Handle incoming data from server
|
|
504
|
+
socket.on("data", (data) => {
|
|
505
|
+
this._handleWebSocketData(socket, data);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// Process any buffered data from the upgrade
|
|
509
|
+
if (head && head.length > 0) {
|
|
510
|
+
this._handleWebSocketData(socket, head);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
resolve(connection);
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
req.on("error", reject);
|
|
517
|
+
req.end();
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Send data through socket as WebSocket frame
|
|
523
|
+
* @param {net.Socket} socket - Raw socket
|
|
524
|
+
* @param {string|Buffer} data - Data to send
|
|
525
|
+
* @private
|
|
526
|
+
*/
|
|
527
|
+
_sendData(socket, data) {
|
|
528
|
+
const frame = FrameEncoder.encode(
|
|
529
|
+
0x1, // Text frame opcode
|
|
530
|
+
Buffer.isBuffer(data) ? data : Buffer.from(data, "utf8"),
|
|
531
|
+
);
|
|
532
|
+
socket.write(frame);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Close the HTTP server gracefully
|
|
537
|
+
* @returns {Promise<void>}
|
|
538
|
+
*/
|
|
539
|
+
async closeServer() {
|
|
540
|
+
return new Promise((resolve) => {
|
|
541
|
+
if (this.server) {
|
|
542
|
+
// Close all active connections
|
|
543
|
+
this.connections.forEach((connection) => {
|
|
544
|
+
if (connection.readyState === 1) {
|
|
545
|
+
this._sendCloseFrame(connection.socket, 1001, "Server going away");
|
|
546
|
+
connection.socket.end();
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
// Close the server
|
|
551
|
+
this.server.close(() => {
|
|
552
|
+
this.connections.clear();
|
|
553
|
+
resolve();
|
|
554
|
+
});
|
|
555
|
+
} else {
|
|
556
|
+
resolve();
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Get server statistics
|
|
563
|
+
* @returns {Object} Statistics object containing connection count, uptime, etc.
|
|
564
|
+
*/
|
|
565
|
+
getStats() {
|
|
566
|
+
return {
|
|
567
|
+
connections: this.connections.size,
|
|
568
|
+
uptime: this.server ? Date.now() - this.server.startTime : 0,
|
|
569
|
+
host: this.config.host,
|
|
570
|
+
port: this.config.port,
|
|
571
|
+
driver: "http",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
export default HTTPDriver;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license MIT
|
|
3
|
+
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
+
* SPDX-License-Identifier: MIT
|
|
5
|
+
* @module @aetherframework/src/drivers/index
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const drivers = {
|
|
9
|
+
tcp: () => import('./tcp-driver.js'),
|
|
10
|
+
tls: () => import('./tls-driver.js'),
|
|
11
|
+
http: () => import('./http-driver.js'),
|
|
12
|
+
memory: () => import('./memory-driver.js')
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get driver class by name
|
|
17
|
+
* @param {string} name - Driver name
|
|
18
|
+
* @returns {Promise<Class>} Driver class
|
|
19
|
+
*/
|
|
20
|
+
export async function getDriver(name) {
|
|
21
|
+
const loader = drivers[name];
|
|
22
|
+
if (!loader) {
|
|
23
|
+
throw new Error(`Unsupported driver: ${name}. Available: ${Object.keys(drivers).join(', ')}`);
|
|
24
|
+
}
|
|
25
|
+
const module = await loader();
|
|
26
|
+
return module.default;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export default { getDriver };
|