@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present, AetherFramework Contributors.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aetherframework/websocket",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Zero-dependency WebSocket server implementation for AetherJS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
"files": [
|
|
44
44
|
"index.js",
|
|
45
45
|
"server.js",
|
|
46
|
-
"src/",
|
|
47
46
|
"connection.js",
|
|
48
47
|
"README.md",
|
|
49
48
|
"LICENSE"
|
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/core/ConnectionManager
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class ConnectionManager {
|
|
9
|
-
constructor() {
|
|
10
|
-
this.connections = new Map();
|
|
11
|
-
this.groups = new Map();
|
|
12
|
-
this.stats = {
|
|
13
|
-
totalConnections: 0,
|
|
14
|
-
activeConnections: 0,
|
|
15
|
-
messagesSent: 0,
|
|
16
|
-
messagesReceived: 0,
|
|
17
|
-
bytesSent: 0,
|
|
18
|
-
bytesReceived: 0
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Add a new connection
|
|
24
|
-
* @param {Object} connection - Connection object
|
|
25
|
-
* @returns {Object} Enhanced connection object
|
|
26
|
-
*/
|
|
27
|
-
add(connection) {
|
|
28
|
-
const enhancedConnection = {
|
|
29
|
-
...connection,
|
|
30
|
-
groups: new Set(),
|
|
31
|
-
metadata: {},
|
|
32
|
-
createdAt: Date.now(),
|
|
33
|
-
lastActivity: Date.now(),
|
|
34
|
-
|
|
35
|
-
// Enhanced send method with statistics
|
|
36
|
-
send: (data, options = {}) => {
|
|
37
|
-
const result = connection.send(data, options);
|
|
38
|
-
if (result) {
|
|
39
|
-
this.stats.messagesSent++;
|
|
40
|
-
this.stats.bytesSent += Buffer.byteLength(data);
|
|
41
|
-
connection.lastActivity = Date.now();
|
|
42
|
-
}
|
|
43
|
-
return result;
|
|
44
|
-
},
|
|
45
|
-
|
|
46
|
-
// Enhanced close method
|
|
47
|
-
close: (code = 1000, reason = '') => {
|
|
48
|
-
this.remove(connection.id);
|
|
49
|
-
return connection.close(code, reason);
|
|
50
|
-
},
|
|
51
|
-
|
|
52
|
-
// Join a group
|
|
53
|
-
join: (groupName) => {
|
|
54
|
-
if (!this.groups.has(groupName)) {
|
|
55
|
-
this.groups.set(groupName, new Set());
|
|
56
|
-
}
|
|
57
|
-
this.groups.get(groupName).add(connection.id);
|
|
58
|
-
enhancedConnection.groups.add(groupName);
|
|
59
|
-
return true;
|
|
60
|
-
},
|
|
61
|
-
|
|
62
|
-
// Leave a group
|
|
63
|
-
leave: (groupName) => {
|
|
64
|
-
const group = this.groups.get(groupName);
|
|
65
|
-
if (group) {
|
|
66
|
-
group.delete(connection.id);
|
|
67
|
-
enhancedConnection.groups.delete(groupName);
|
|
68
|
-
return true;
|
|
69
|
-
}
|
|
70
|
-
return false;
|
|
71
|
-
},
|
|
72
|
-
|
|
73
|
-
// Get connection statistics
|
|
74
|
-
getStats: () => ({
|
|
75
|
-
id: connection.id,
|
|
76
|
-
readyState: connection.readyState,
|
|
77
|
-
uptime: Date.now() - enhancedConnection.createdAt,
|
|
78
|
-
lastActivity: enhancedConnection.lastActivity,
|
|
79
|
-
groups: Array.from(enhancedConnection.groups),
|
|
80
|
-
metadata: enhancedConnection.metadata,
|
|
81
|
-
remoteAddress: connection.remoteAddress,
|
|
82
|
-
remotePort: connection.remotePort
|
|
83
|
-
})
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
this.connections.set(connection.id, enhancedConnection);
|
|
87
|
-
this.stats.totalConnections++;
|
|
88
|
-
this.stats.activeConnections++;
|
|
89
|
-
|
|
90
|
-
// Set up cleanup on close
|
|
91
|
-
connection.on('close', () => {
|
|
92
|
-
this.remove(connection.id);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
return enhancedConnection;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Remove a connection
|
|
100
|
-
* @param {string} connectionId - Connection ID
|
|
101
|
-
* @returns {boolean} Success status
|
|
102
|
-
*/
|
|
103
|
-
remove(connectionId) {
|
|
104
|
-
const connection = this.connections.get(connectionId);
|
|
105
|
-
if (connection) {
|
|
106
|
-
// Remove from all groups
|
|
107
|
-
connection.groups.forEach(groupName => {
|
|
108
|
-
const group = this.groups.get(groupName);
|
|
109
|
-
if (group) {
|
|
110
|
-
group.delete(connectionId);
|
|
111
|
-
if (group.size === 0) {
|
|
112
|
-
this.groups.delete(groupName);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
this.connections.delete(connectionId);
|
|
118
|
-
this.stats.activeConnections--;
|
|
119
|
-
return true;
|
|
120
|
-
}
|
|
121
|
-
return false;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Get connection by ID
|
|
126
|
-
* @param {string} connectionId - Connection ID
|
|
127
|
-
* @returns {Object|null} Connection object or null
|
|
128
|
-
*/
|
|
129
|
-
get(connectionId) {
|
|
130
|
-
return this.connections.get(connectionId) || null;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* Get all connections
|
|
135
|
-
* @returns {Array} Array of connection objects
|
|
136
|
-
*/
|
|
137
|
-
getAll() {
|
|
138
|
-
return Array.from(this.connections.values());
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Get connections by group
|
|
143
|
-
* @param {string} groupName - Group name
|
|
144
|
-
* @returns {Array} Array of connections in the group
|
|
145
|
-
*/
|
|
146
|
-
getGroup(groupName) {
|
|
147
|
-
const group = this.groups.get(groupName);
|
|
148
|
-
if (!group) return [];
|
|
149
|
-
|
|
150
|
-
return Array.from(group)
|
|
151
|
-
.map(id => this.get(id))
|
|
152
|
-
.filter(conn => conn !== null && conn.readyState === 1);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Broadcast to a specific group
|
|
157
|
-
* @param {string} groupName - Group name
|
|
158
|
-
* @param {string|Buffer} message - Message to send
|
|
159
|
-
* @returns {number} Number of connections that received the message
|
|
160
|
-
*/
|
|
161
|
-
broadcastToGroup(groupName, message) {
|
|
162
|
-
const connections = this.getGroup(groupName);
|
|
163
|
-
let successCount = 0;
|
|
164
|
-
|
|
165
|
-
connections.forEach(connection => {
|
|
166
|
-
if (connection.send(message)) {
|
|
167
|
-
successCount++;
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
return successCount;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* Filter connections based on criteria
|
|
176
|
-
* @param {Function} filterFn - Filter function
|
|
177
|
-
* @returns {Array} Filtered connections
|
|
178
|
-
*/
|
|
179
|
-
filter(filterFn) {
|
|
180
|
-
return this.getAll().filter(filterFn);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Get connection statistics
|
|
185
|
-
* @returns {Object} Statistics object
|
|
186
|
-
*/
|
|
187
|
-
getStats() {
|
|
188
|
-
return {
|
|
189
|
-
...this.stats,
|
|
190
|
-
groups: this.groups.size,
|
|
191
|
-
timestamp: Date.now()
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Clear all connections
|
|
197
|
-
*/
|
|
198
|
-
clear() {
|
|
199
|
-
this.connections.clear();
|
|
200
|
-
this.groups.clear();
|
|
201
|
-
this.stats.activeConnections = 0;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
/**
|
|
205
|
-
* Get connection count
|
|
206
|
-
* @returns {number} Number of connections
|
|
207
|
-
*/
|
|
208
|
-
count() {
|
|
209
|
-
return this.connections.size;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export default ConnectionManager;
|
package/src/core/FrameParser.js
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @license MIT
|
|
3
|
-
* Copyright (c) 2026-present AetherFramework Contributors.
|
|
4
|
-
* SPDX-License-Identifier: MIT
|
|
5
|
-
* @module @aetherframework/src/core/FrameParser
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
class FrameParser {
|
|
9
|
-
/**
|
|
10
|
-
* Parse raw buffer data into WebSocket frames
|
|
11
|
-
* @param {Buffer} buffer - Raw incoming data
|
|
12
|
-
* @param {number} maxPayload - Maximum allowed payload size
|
|
13
|
-
* @returns {Object} { frames: Array, remaining: Buffer }
|
|
14
|
-
*/
|
|
15
|
-
static parse(buffer, maxPayload = 1024 * 1024) {
|
|
16
|
-
const frames = [];
|
|
17
|
-
let offset = 0;
|
|
18
|
-
|
|
19
|
-
while (offset < buffer.length) {
|
|
20
|
-
// Need at least 2 bytes for basic header
|
|
21
|
-
if (buffer.length - offset < 2) break;
|
|
22
|
-
|
|
23
|
-
const byte1 = buffer[offset];
|
|
24
|
-
const byte2 = buffer[offset + 1];
|
|
25
|
-
|
|
26
|
-
const fin = (byte1 & 0x80) !== 0;
|
|
27
|
-
const opcode = byte1 & 0x0F;
|
|
28
|
-
const mask = (byte2 & 0x80) !== 0;
|
|
29
|
-
let payloadLength = byte2 & 0x7F;
|
|
30
|
-
|
|
31
|
-
let headerSize = 2;
|
|
32
|
-
let payloadOffset = offset + 2;
|
|
33
|
-
|
|
34
|
-
// Handle extended payload length
|
|
35
|
-
if (payloadLength === 126) {
|
|
36
|
-
if (buffer.length - offset < 4) break;
|
|
37
|
-
payloadLength = buffer.readUInt16BE(offset + 2);
|
|
38
|
-
headerSize += 2;
|
|
39
|
-
payloadOffset += 2;
|
|
40
|
-
} else if (payloadLength === 127) {
|
|
41
|
-
if (buffer.length - offset < 10) break;
|
|
42
|
-
// JavaScript numbers are safe up to 2^53, but WS spec says 2^63
|
|
43
|
-
// We use BigInt for safety then convert if safe, or throw if too large
|
|
44
|
-
const bigLen = buffer.readBigUInt64BE(offset + 2);
|
|
45
|
-
if (bigLen > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
46
|
-
throw new Error('Payload too large');
|
|
47
|
-
}
|
|
48
|
-
payloadLength = Number(bigLen);
|
|
49
|
-
headerSize += 8;
|
|
50
|
-
payloadOffset += 8;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Check payload size limit
|
|
54
|
-
if (payloadLength > maxPayload) {
|
|
55
|
-
throw new Error(`Payload exceeds maximum size: ${payloadLength} > ${maxPayload}`);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Handle masking key
|
|
59
|
-
let maskingKey = null;
|
|
60
|
-
if (mask) {
|
|
61
|
-
if (buffer.length - offset < headerSize + 4) break;
|
|
62
|
-
maskingKey = buffer.slice(offset + headerSize, offset + headerSize + 4);
|
|
63
|
-
headerSize += 4;
|
|
64
|
-
payloadOffset += 4;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Check if full payload is available
|
|
68
|
-
if (buffer.length - offset < headerSize + payloadLength) break;
|
|
69
|
-
|
|
70
|
-
// Extract payload
|
|
71
|
-
let payload = buffer.slice(payloadOffset, payloadOffset + payloadLength);
|
|
72
|
-
|
|
73
|
-
// Unmask if necessary
|
|
74
|
-
if (mask && maskingKey) {
|
|
75
|
-
payload = this._unmask(payload, maskingKey);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
frames.push({
|
|
79
|
-
fin,
|
|
80
|
-
opcode,
|
|
81
|
-
mask,
|
|
82
|
-
payloadLength,
|
|
83
|
-
payload
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
offset += headerSize + payloadLength;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return {
|
|
90
|
-
frames,
|
|
91
|
-
remaining: buffer.slice(offset)
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Unmask payload using XOR operation
|
|
97
|
-
* @param {Buffer} payload
|
|
98
|
-
* @param {Buffer} maskingKey
|
|
99
|
-
* @returns {Buffer} Unmasked payload
|
|
100
|
-
* @private
|
|
101
|
-
*/
|
|
102
|
-
static _unmask(payload, maskingKey) {
|
|
103
|
-
const len = payload.length;
|
|
104
|
-
const unmasked = Buffer.allocUnsafe(len);
|
|
105
|
-
|
|
106
|
-
// Optimized loop for unmasking
|
|
107
|
-
for (let i = 0; i < len; i++) {
|
|
108
|
-
unmasked[i] = payload[i] ^ maskingKey[i % 4];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return unmasked;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export default FrameParser;
|
|
@@ -1,93 +0,0 @@
|
|
|
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;
|
|
@@ -1,186 +0,0 @@
|
|
|
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;
|