@boo-labs/server 0.0.0-stage → 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.
- package/README.md +229 -2
- package/index.mjs +23 -0
- package/package.json +71 -4
- package/source/application/Kernel/index.mjs +1137 -0
- package/source/application/Server/index.mjs +1201 -0
- package/source/application/configurations/Http2CleartextServerConfiguration/index.mjs +168 -0
- package/source/application/configurations/Http2SecureServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/HttpServerConfiguration/index.mjs +169 -0
- package/source/application/configurations/HttpsServerConfiguration/index.mjs +172 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/CORSOptionsValue.mjs +158 -0
- package/source/application/configurations/ServerConfiguration/Options/CORSOptions/index.mjs +87 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/CSRFOptionsValue.mjs +178 -0
- package/source/application/configurations/ServerConfiguration/Options/CSRFOptions/index.mjs +80 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/CompressOptionsValue.mjs +126 -0
- package/source/application/configurations/ServerConfiguration/Options/CompressOptions/index.mjs +81 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/CookieParserOptionsValue.mjs +107 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieParserOptions/index.mjs +82 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/CookieSessionOptionsValue.mjs +182 -0
- package/source/application/configurations/ServerConfiguration/Options/CookieSessionOptions/index.mjs +86 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/ListenOptionsValue.mjs +95 -0
- package/source/application/configurations/ServerConfiguration/Options/ListenOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/MiddlewareOption.mjs +102 -0
- package/source/application/configurations/ServerConfiguration/Options/Options.mjs +105 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/RateLimitOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/RateLimitOptions/index.mjs +84 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/RequestOptionsValue.mjs +108 -0
- package/source/application/configurations/ServerConfiguration/Options/RequestOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/ResponseOptionsValue.mjs +101 -0
- package/source/application/configurations/ServerConfiguration/Options/ResponseOptions/index.mjs +47 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/SecurityHeadersOptionsValue.mjs +272 -0
- package/source/application/configurations/ServerConfiguration/Options/SecurityHeadersOptions/index.mjs +85 -0
- package/source/application/configurations/ServerConfiguration/Options/ServerOption.mjs +50 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/StaticOptionsValue.mjs +164 -0
- package/source/application/configurations/ServerConfiguration/Options/StaticOptions/index.mjs +89 -0
- package/source/application/configurations/ServerConfiguration/index.mjs +781 -0
- package/source/application/resolvers/TransportResolver/index.mjs +119 -0
- package/source/application/use-cases/CloseServer/index.mjs +85 -0
- package/source/application/use-cases/ListenServer/index.mjs +98 -0
- package/source/core/EventSystem/index.mjs +110 -0
- package/source/core/Exchange/ExchangeContext.mjs +291 -0
- package/source/core/Exchange/Telemetry.mjs +193 -0
- package/source/core/Exchange/index.mjs +604 -0
- package/source/core/ExchangeLayerBuilder/index.mjs +133 -0
- package/source/core/IPCMain/IPCMain.mjs +507 -0
- package/source/core/IPCMain/IPCProtocol.mjs +441 -0
- package/source/core/IPCMain/IPCServerChannel.mjs +331 -0
- package/source/core/IPCMain/WebSocketAdapter.mjs +189 -0
- package/source/core/Request/BodyParser/BodyParser.mjs +197 -0
- package/source/core/Request/BodyParser/JSONParser.mjs +70 -0
- package/source/core/Request/BodyParser/MultipartParser.mjs +612 -0
- package/source/core/Request/BodyParser/URLEncodedParser.mjs +71 -0
- package/source/core/Request/RequestHeaders.mjs +177 -0
- package/source/core/Request/index.mjs +805 -0
- package/source/core/Response/ResponseHeaders.mjs +402 -0
- package/source/core/Response/index.mjs +1370 -0
- package/source/core/middlewares/CORSMiddleware/index.mjs +289 -0
- package/source/core/middlewares/CSRFMiddleware/index.mjs +284 -0
- package/source/core/middlewares/CompressMiddleware/index.mjs +460 -0
- package/source/core/middlewares/CookieParserMiddleware/index.mjs +194 -0
- package/source/core/middlewares/CookieSessionMiddleware/index.mjs +460 -0
- package/source/core/middlewares/RateLimitMiddleware/index.mjs +245 -0
- package/source/core/middlewares/SecurityHeadersMiddleware/index.mjs +151 -0
- package/source/core/middlewares/StaticMiddleware/index.mjs +628 -0
- package/source/core/routing/ExchangeLayer/ExchangeMatcher.mjs +127 -0
- package/source/core/routing/ExchangeLayer/index.mjs +254 -0
- package/source/core/routing/Pipeline/index.mjs +157 -0
- package/source/core/routing/Router/Route.mjs +184 -0
- package/source/core/routing/Router/index.mjs +523 -0
- package/source/core/routing/RouterRegister/OptionsMiddleware.mjs +147 -0
- package/source/core/routing/RouterRegister/index.mjs +150 -0
- package/source/core/routing/RouterSystem/index.mjs +225 -0
- package/source/core/value-objects/Address/index.mjs +281 -0
- package/source/core/value-objects/Endpoint/index.mjs +293 -0
- package/source/index.mjs +62 -0
- package/source/infrastructure/adapters/Http2Adapter/index.mjs +144 -0
- package/source/infrastructure/adapters/Http2CleartextAdapter/index.mjs +147 -0
- package/source/infrastructure/adapters/HttpAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/HttpsAdapter/index.mjs +124 -0
- package/source/infrastructure/adapters/TransportAdapter/createWebSocketOpenRequest.mjs +843 -0
- package/source/infrastructure/adapters/TransportAdapter/index.mjs +555 -0
- package/source/shared/Identifier/index.mjs +671 -0
- package/source/shared/constants/symbols/brands/index.mjs +45 -0
- package/source/shared/domain/UUIDV7/index.mjs +346 -0
- package/source/shared/errors/BaseError/index.mjs +157 -0
- package/source/shared/errors/IPCError/index.mjs +97 -0
- package/source/shared/errors/MiddlewareError/index.mjs +106 -0
- package/source/shared/errors/PathnamePatternError/index.mjs +103 -0
- package/source/shared/errors/RequestError/index.mjs +116 -0
- package/source/shared/errors/ResponseError/index.mjs +140 -0
- package/source/shared/errors/RouterError/index.mjs +90 -0
- package/source/shared/errors/ServerConfigurationError/index.mjs +551 -0
- package/source/shared/errors/ServerError/index.mjs +127 -0
- package/source/shared/errors/WebSocketError/index.mjs +58 -0
- package/source/shared/outcome/ErrorSchema.mjs +361 -0
- package/source/shared/outcome/Result/index.mjs +239 -0
- package/source/shared/outcome/Validation/index.mjs +208 -0
- package/source/shared/validations/Middleware/index.mjs +160 -0
- package/source/shared/validations/PathnamePattern/index.mjs +256 -0
- package/source/shared/validations/Server/NotFoundHandler/index.mjs +94 -0
- package/source/shared/validations/Server/Router/index.mjs +68 -0
- package/source/shared/validations/Server/ServerConfiguration/index.mjs +68 -0
- package/source/shared/validations/Server/SystemErrorHandler/index.mjs +94 -0
- package/source/shared/validations/ServerConfiguration/CORSOptions/index.mjs +310 -0
- package/source/shared/validations/ServerConfiguration/CSRFOptions/index.mjs +299 -0
- package/source/shared/validations/ServerConfiguration/CompressOptions/index.mjs +204 -0
- package/source/shared/validations/ServerConfiguration/CookieParserOptions/index.mjs +102 -0
- package/source/shared/validations/ServerConfiguration/CookieSessionOptions/index.mjs +332 -0
- package/source/shared/validations/ServerConfiguration/ListenOptions/index.mjs +178 -0
- package/source/shared/validations/ServerConfiguration/RateLimitOptions/index.mjs +229 -0
- package/source/shared/validations/ServerConfiguration/RequestOptions/index.mjs +132 -0
- package/source/shared/validations/ServerConfiguration/ResponseOptions/index.mjs +122 -0
- package/source/shared/validations/ServerConfiguration/SecurityHeadersOptions/index.mjs +137 -0
- package/source/shared/validations/ServerConfiguration/StaticOptions/index.mjs +248 -0
- package/types/index.d.ts +719 -0
|
@@ -0,0 +1,843 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* @file Provides WebSocket upgrade and connection utilities for HTTP/1.1
|
|
4
|
+
* transports.
|
|
5
|
+
*
|
|
6
|
+
* @description Implements the RFC 6455 handshake and the low-level WebSocket
|
|
7
|
+
* connection required by the HTTP/1.1 transport. It includes subprotocol
|
|
8
|
+
* negotiation, message size limits, and full frame lifecycle management
|
|
9
|
+
* (fragmentation, ping/pong, and close).
|
|
10
|
+
*
|
|
11
|
+
* @module WebSocketUpgrade
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ━━ IMPORT MODULES ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
15
|
+
// » IMPORT NATIVE NODEJS MODULES
|
|
16
|
+
import crypto from 'node:crypto';
|
|
17
|
+
|
|
18
|
+
// ━━ TYPE DEFINITIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
19
|
+
/**
|
|
20
|
+
* Underlying TCP socket used by a WebSocket connection.
|
|
21
|
+
*
|
|
22
|
+
* @typedef {import('node:net').Socket} WebSocketSocket
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* HTTP/1.1 request received during a WebSocket upgrade.
|
|
27
|
+
*
|
|
28
|
+
* @typedef {import('node:http').IncomingMessage} WebSocketUpgradeRequest
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Listener for incoming WebSocket messages.
|
|
33
|
+
*
|
|
34
|
+
* @callback WebSocketMessageListener
|
|
35
|
+
* @param {string|Buffer} data - Incoming text or binary message.
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Listener for WebSocket close events.
|
|
41
|
+
*
|
|
42
|
+
* @callback WebSocketCloseListener
|
|
43
|
+
* @returns {void}
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Listener for WebSocket error events.
|
|
48
|
+
*
|
|
49
|
+
* @callback WebSocketErrorListener
|
|
50
|
+
* @param {Error} error - Error reported by the WebSocket connection.
|
|
51
|
+
* @returns {void}
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Registers a WebSocket event listener.
|
|
56
|
+
*
|
|
57
|
+
* @typedef {(
|
|
58
|
+
* ((event: 'message', listener: WebSocketMessageListener) => void) &
|
|
59
|
+
* ((event: 'close', listener: WebSocketCloseListener) => void) &
|
|
60
|
+
* ((event: 'error', listener: WebSocketErrorListener) => void)
|
|
61
|
+
* )} WebSocketOn
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Represents an established WebSocket connection contract.
|
|
66
|
+
*
|
|
67
|
+
* @typedef {object} WebSocketConnection
|
|
68
|
+
* @property {(data: string|Buffer) => void} send - Sends a text or binary WebSocket message.
|
|
69
|
+
* @property {WebSocketOn} on - Registers a listener for WebSocket events.
|
|
70
|
+
* @property {() => void} close - Closes the WebSocket connection.
|
|
71
|
+
* @property {number} readyState - Current transport-defined WebSocket ready state.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Transport-level request used to accept or reject a WebSocket handshake.
|
|
76
|
+
*
|
|
77
|
+
* @typedef {object} WebSocketOpenRequest
|
|
78
|
+
* @property {string} url - The requested WebSocket URL.
|
|
79
|
+
* @property {Record<string, string|string[]|undefined>} headers - Headers received during the WebSocket handshake.
|
|
80
|
+
* @property {(selectedProtocol?: string) => WebSocketConnection} accept - Accepts the handshake. Optionally selects a subprotocol.
|
|
81
|
+
* @property {(code?: number, message?: string) => void} reject - Rejects the WebSocket handshake.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Represents a parsed WebSocket frame.
|
|
86
|
+
*
|
|
87
|
+
* @typedef {object} WebSocketFrame
|
|
88
|
+
* @property {boolean} fin - Whether this is the final fragment of the message.
|
|
89
|
+
* @property {number} opcode - Frame opcode.
|
|
90
|
+
* @property {Buffer} payload - Unmasked frame payload.
|
|
91
|
+
* @property {number} length - Total frame length in bytes, including header.
|
|
92
|
+
*/
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Options accepted by {@link createWebSocketOpenRequest}.
|
|
96
|
+
*
|
|
97
|
+
* @typedef {object} WebSocketUpgradeOptions
|
|
98
|
+
* @property {number} [maxMessageSize] - Maximum allowed message size in bytes.
|
|
99
|
+
* @property {string[]} [supportedProtocols] - WebSocket subprotocols supported by the server.
|
|
100
|
+
*/
|
|
101
|
+
|
|
102
|
+
// ━━ CONSTANTS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
103
|
+
/**
|
|
104
|
+
* Magic string defined by RFC 6455 for the WebSocket handshake.
|
|
105
|
+
*
|
|
106
|
+
* @type {string}
|
|
107
|
+
*/
|
|
108
|
+
const WS_MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* WebSocket protocol version supported by this implementation.
|
|
112
|
+
*
|
|
113
|
+
* @type {number}
|
|
114
|
+
*/
|
|
115
|
+
const WS_VERSION = 13;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* WebSocket close code indicating a normal closure.
|
|
119
|
+
*
|
|
120
|
+
* @type {number}
|
|
121
|
+
*/
|
|
122
|
+
const WS_CLOSE_NORMAL = 1000;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* WebSocket close code indicating a protocol error.
|
|
126
|
+
*
|
|
127
|
+
* @type {number}
|
|
128
|
+
*/
|
|
129
|
+
const WS_CLOSE_PROTOCOL_ERROR = 1002;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* WebSocket close code indicating invalid frame payload data.
|
|
133
|
+
*
|
|
134
|
+
* @type {number}
|
|
135
|
+
*/
|
|
136
|
+
const WS_CLOSE_INVALID_PAYLOAD = 1007;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* WebSocket close code indicating a message that is too large to process.
|
|
140
|
+
*
|
|
141
|
+
* @type {number}
|
|
142
|
+
*/
|
|
143
|
+
const WS_CLOSE_MESSAGE_TOO_BIG = 1009;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* WebSocket ready state indicating the connection is open.
|
|
147
|
+
*
|
|
148
|
+
* @type {number}
|
|
149
|
+
*/
|
|
150
|
+
const WS_OPEN = 1;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* WebSocket ready state indicating the closing handshake has started.
|
|
154
|
+
*
|
|
155
|
+
* @type {number}
|
|
156
|
+
*/
|
|
157
|
+
const WS_CLOSING = 2;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* WebSocket ready state indicating the connection is closed.
|
|
161
|
+
*
|
|
162
|
+
* @type {number}
|
|
163
|
+
*/
|
|
164
|
+
const WS_CLOSED = 3;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Default maximum allowed message size in bytes.
|
|
168
|
+
*
|
|
169
|
+
* Prevents memory exhaustion attacks from oversized WebSocket messages,
|
|
170
|
+
* including fragmented messages.
|
|
171
|
+
*
|
|
172
|
+
* @type {number}
|
|
173
|
+
*/
|
|
174
|
+
const DEFAULT_MAX_MESSAGE_SIZE = 10 * 1024 * 1024;
|
|
175
|
+
|
|
176
|
+
// ━━ FUNCTIONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
177
|
+
/**
|
|
178
|
+
* Encodes a WebSocket close code as a two-byte buffer.
|
|
179
|
+
*
|
|
180
|
+
* @private
|
|
181
|
+
* @param {number} code - WebSocket close code.
|
|
182
|
+
* @returns {Buffer} Two-byte close code buffer.
|
|
183
|
+
*/
|
|
184
|
+
function encodeCloseCode(code) {
|
|
185
|
+
const buffer = Buffer.alloc(2);
|
|
186
|
+
|
|
187
|
+
buffer.writeUInt16BE(code, 0);
|
|
188
|
+
|
|
189
|
+
return buffer;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Determines whether a WebSocket close code is valid per RFC 6455.
|
|
194
|
+
*
|
|
195
|
+
* Valid ranges:
|
|
196
|
+
*
|
|
197
|
+
* - `1000`–`1003`: normal and endpoint-defined closures.
|
|
198
|
+
* - `1007`–`1014`: RFC-defined closure conditions.
|
|
199
|
+
* - `3000`–`4999`: application- and library-defined closures.
|
|
200
|
+
*
|
|
201
|
+
* @private
|
|
202
|
+
* @param {number} code - WebSocket close code.
|
|
203
|
+
* @returns {boolean} `true` when the code is valid.
|
|
204
|
+
*/
|
|
205
|
+
function isValidCloseCode(code) {
|
|
206
|
+
return (
|
|
207
|
+
(code >= 1000 && code <= 1003) ||
|
|
208
|
+
(code >= 1007 && code <= 1014) ||
|
|
209
|
+
(code >= 3000 && code <= 4999)
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Determines whether a buffer contains valid UTF-8 data.
|
|
215
|
+
*
|
|
216
|
+
* @private
|
|
217
|
+
* @param {Buffer} buffer - Buffer to validate.
|
|
218
|
+
* @returns {boolean} `true` when the buffer is valid UTF-8.
|
|
219
|
+
*/
|
|
220
|
+
function isValidUTF8(buffer) {
|
|
221
|
+
try {
|
|
222
|
+
new TextDecoder('utf-8', { fatal: true }).decode(buffer);
|
|
223
|
+
|
|
224
|
+
return true;
|
|
225
|
+
} catch {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Creates a WebSocket frame.
|
|
232
|
+
*
|
|
233
|
+
* Server-to-client frames are never masked.
|
|
234
|
+
*
|
|
235
|
+
* @private
|
|
236
|
+
* @param {number} opcode - WebSocket frame opcode.
|
|
237
|
+
* @param {Buffer} payload - Frame payload.
|
|
238
|
+
* @returns {Buffer} Encoded WebSocket frame.
|
|
239
|
+
*/
|
|
240
|
+
function createWebSocketFrame(opcode, payload) {
|
|
241
|
+
const length = payload.length;
|
|
242
|
+
|
|
243
|
+
if (length <= 125) {
|
|
244
|
+
const frame = Buffer.alloc(2 + length);
|
|
245
|
+
|
|
246
|
+
frame[0] = 0x80 | opcode;
|
|
247
|
+
frame[1] = length;
|
|
248
|
+
payload.copy(frame, 2);
|
|
249
|
+
|
|
250
|
+
return frame;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (length <= 0xffff) {
|
|
254
|
+
const frame = Buffer.alloc(4 + length);
|
|
255
|
+
|
|
256
|
+
frame[0] = 0x80 | opcode;
|
|
257
|
+
frame[1] = 126;
|
|
258
|
+
frame.writeUInt16BE(length, 2);
|
|
259
|
+
payload.copy(frame, 4);
|
|
260
|
+
|
|
261
|
+
return frame;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const frame = Buffer.alloc(10 + length);
|
|
265
|
+
|
|
266
|
+
frame[0] = 0x80 | opcode;
|
|
267
|
+
frame[1] = 127;
|
|
268
|
+
frame.writeBigUInt64BE(BigInt(length), 2);
|
|
269
|
+
payload.copy(frame, 10);
|
|
270
|
+
|
|
271
|
+
return frame;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Parses an incoming WebSocket frame.
|
|
276
|
+
*
|
|
277
|
+
* Client-to-server frames must be masked according to RFC 6455. The returned
|
|
278
|
+
* payload is always unmasked.
|
|
279
|
+
*
|
|
280
|
+
* @private
|
|
281
|
+
* @param {Buffer} buffer - Raw WebSocket bytes.
|
|
282
|
+
* @param {number} maxMessageSize - Maximum allowed message size in bytes.
|
|
283
|
+
* @returns {WebSocketFrame|null} Parsed frame, or `null` when the buffer does not yet contain a complete frame.
|
|
284
|
+
* @throws {Error & { code?: number }} Thrown when the frame violates the WebSocket protocol. The error may carry a numeric `code` indicating the appropriate WebSocket close code.
|
|
285
|
+
*/
|
|
286
|
+
function parseWebSocketFrame(buffer, maxMessageSize) {
|
|
287
|
+
if (buffer.length < 2) {
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const firstByte = buffer[0];
|
|
292
|
+
const secondByte = buffer[1];
|
|
293
|
+
|
|
294
|
+
const fin = (firstByte & 0x80) !== 0;
|
|
295
|
+
const reserved = firstByte & 0x70;
|
|
296
|
+
const opcode = firstByte & 0x0f;
|
|
297
|
+
|
|
298
|
+
if (reserved !== 0) {
|
|
299
|
+
throw new Error('Reserved WebSocket bits are not supported.');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
opcode !== 0x0 &&
|
|
304
|
+
opcode !== 0x1 &&
|
|
305
|
+
opcode !== 0x2 &&
|
|
306
|
+
opcode !== 0x8 &&
|
|
307
|
+
opcode !== 0x9 &&
|
|
308
|
+
opcode !== 0xa
|
|
309
|
+
) {
|
|
310
|
+
throw new Error(`Unsupported WebSocket opcode: ${opcode}.`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const isMasked = (secondByte & 0x80) !== 0;
|
|
314
|
+
|
|
315
|
+
if (!isMasked) {
|
|
316
|
+
throw new Error('Client WebSocket frames must be masked.');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
let payloadLength = secondByte & 0x7f;
|
|
320
|
+
let offset = 2;
|
|
321
|
+
|
|
322
|
+
if (payloadLength === 126) {
|
|
323
|
+
if (buffer.length < 4) {
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
payloadLength = buffer.readUInt16BE(2);
|
|
328
|
+
offset = 4;
|
|
329
|
+
} else if (payloadLength === 127) {
|
|
330
|
+
if (buffer.length < 10) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const length = buffer.readBigUInt64BE(2);
|
|
335
|
+
|
|
336
|
+
if (length > BigInt(Number.MAX_SAFE_INTEGER)) {
|
|
337
|
+
throw new Error('WebSocket payload is too large.');
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
payloadLength = Number(length);
|
|
341
|
+
offset = 10;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const isControlFrame = opcode >= 0x8;
|
|
345
|
+
|
|
346
|
+
if (isControlFrame && (!fin || payloadLength > 125)) {
|
|
347
|
+
throw new Error('Invalid WebSocket control frame.');
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (!isControlFrame && payloadLength > maxMessageSize) {
|
|
351
|
+
const error = /** @type {Error & { code?: number }} */ (
|
|
352
|
+
new Error(
|
|
353
|
+
`WebSocket payload size (${payloadLength} bytes) exceeds maximum allowed message size (${maxMessageSize} bytes).`,
|
|
354
|
+
)
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
error.code = WS_CLOSE_MESSAGE_TOO_BIG;
|
|
358
|
+
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (buffer.length < offset + 4) {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const mask = buffer.subarray(offset, offset + 4);
|
|
367
|
+
offset += 4;
|
|
368
|
+
|
|
369
|
+
if (buffer.length < offset + payloadLength) {
|
|
370
|
+
return null;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const payload = Buffer.from(buffer.subarray(offset, offset + payloadLength));
|
|
374
|
+
|
|
375
|
+
for (let index = 0; index < payload.length; index += 1) {
|
|
376
|
+
payload[index] ^= mask[index % 4];
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
fin,
|
|
381
|
+
opcode,
|
|
382
|
+
payload,
|
|
383
|
+
length: offset + payloadLength,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Creates a WebSocket connection over a TCP socket.
|
|
389
|
+
*
|
|
390
|
+
* @param {WebSocketSocket} socket - Underlying TCP socket.
|
|
391
|
+
* @param {Buffer} head - Bytes already read during the HTTP upgrade.
|
|
392
|
+
* @param {number} maxMessageSize - Maximum allowed message size in bytes.
|
|
393
|
+
* @returns {WebSocketConnection} WebSocket connection.
|
|
394
|
+
* @example
|
|
395
|
+
* ```js
|
|
396
|
+
* const connection = createWebSocketConnection(socket, head, 10 * 1024 * 1024);
|
|
397
|
+
* connection.on('message', data => console.log(data));
|
|
398
|
+
* connection.send('hello');
|
|
399
|
+
* connection.close();
|
|
400
|
+
* ```
|
|
401
|
+
*/
|
|
402
|
+
function createWebSocketConnection(socket, head, maxMessageSize) {
|
|
403
|
+
/**
|
|
404
|
+
* Registered message listeners.
|
|
405
|
+
*
|
|
406
|
+
* @type {((data: string|Buffer) => void)[]}
|
|
407
|
+
*/
|
|
408
|
+
const messageListeners = [];
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Registered close listeners.
|
|
412
|
+
*
|
|
413
|
+
* @type {(() => void)[]}
|
|
414
|
+
*/
|
|
415
|
+
const closeListeners = [];
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Registered error listeners.
|
|
419
|
+
*
|
|
420
|
+
* @type {((error: Error) => void)[]}
|
|
421
|
+
*/
|
|
422
|
+
const errorListeners = [];
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Buffered bytes pending parsing.
|
|
426
|
+
*
|
|
427
|
+
* @type {Buffer}
|
|
428
|
+
*/
|
|
429
|
+
let bufferQueue = Buffer.alloc(0);
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* Current connection ready state.
|
|
433
|
+
*
|
|
434
|
+
* @type {number}
|
|
435
|
+
*/
|
|
436
|
+
let readyState = WS_OPEN;
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Opcode of the currently in-progress fragmented message, or `null`.
|
|
440
|
+
*
|
|
441
|
+
* @type {number|null}
|
|
442
|
+
*/
|
|
443
|
+
let fragmentedOpcode = null;
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Accumulated fragments of the in-progress fragmented message.
|
|
447
|
+
*
|
|
448
|
+
* @type {Buffer[]}
|
|
449
|
+
*/
|
|
450
|
+
let fragmentedPayload = [];
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Accumulated byte size of the in-progress fragmented message.
|
|
454
|
+
*
|
|
455
|
+
* @type {number}
|
|
456
|
+
*/
|
|
457
|
+
let fragmentedSize = 0;
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Writes a WebSocket frame to the underlying socket.
|
|
461
|
+
*
|
|
462
|
+
* @param {number} opcode - Frame opcode.
|
|
463
|
+
* @param {Buffer} payload - Frame payload.
|
|
464
|
+
* @throws {Error} Thrown when the WebSocket connection is not open.
|
|
465
|
+
*/
|
|
466
|
+
const sendFrame = (opcode, payload) => {
|
|
467
|
+
if (readyState !== WS_OPEN) {
|
|
468
|
+
throw new Error('WebSocket connection is not open.');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
socket.write(createWebSocketFrame(opcode, payload));
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Sends a WebSocket close frame and ends the underlying socket.
|
|
476
|
+
*
|
|
477
|
+
* @param {Buffer} payload - Close frame payload.
|
|
478
|
+
*/
|
|
479
|
+
const sendClose = payload => {
|
|
480
|
+
if (readyState === WS_OPEN) {
|
|
481
|
+
readyState = WS_CLOSING;
|
|
482
|
+
socket.write(createWebSocketFrame(0x8, payload));
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
socket.end();
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Emits a complete WebSocket message to all registered listeners.
|
|
490
|
+
*
|
|
491
|
+
* Text frames are validated for UTF-8 correctness. Invalid UTF-8 payloads
|
|
492
|
+
* trigger a `1007` close frame per RFC 6455 §5.6.
|
|
493
|
+
*
|
|
494
|
+
* @param {number} opcode - Original message opcode.
|
|
495
|
+
* @param {Buffer} payload - Complete message payload.
|
|
496
|
+
* @returns {boolean} `true` when the message was emitted, `false` when it was rejected.
|
|
497
|
+
*/
|
|
498
|
+
const emitMessage = (opcode, payload) => {
|
|
499
|
+
if (opcode === 0x1 && !isValidUTF8(payload)) {
|
|
500
|
+
sendClose(encodeCloseCode(WS_CLOSE_INVALID_PAYLOAD));
|
|
501
|
+
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const data = opcode === 0x1 ? payload.toString('utf8') : payload;
|
|
506
|
+
|
|
507
|
+
messageListeners.forEach(listener => listener(data));
|
|
508
|
+
|
|
509
|
+
return true;
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Handles a parsed WebSocket frame.
|
|
514
|
+
*
|
|
515
|
+
* @param {WebSocketFrame} frame - Parsed WebSocket frame.
|
|
516
|
+
* @returns {boolean} Whether the frame was valid.
|
|
517
|
+
*/
|
|
518
|
+
const handleFrame = frame => {
|
|
519
|
+
if (frame.opcode === 0x8) {
|
|
520
|
+
if (frame.payload.length === 1) {
|
|
521
|
+
return false;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
if (frame.payload.length >= 2) {
|
|
525
|
+
const code = frame.payload.readUInt16BE(0);
|
|
526
|
+
|
|
527
|
+
if (!isValidCloseCode(code)) {
|
|
528
|
+
return false;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
if (frame.payload.length > 2 && !isValidUTF8(frame.payload.subarray(2))) {
|
|
532
|
+
sendClose(encodeCloseCode(WS_CLOSE_INVALID_PAYLOAD));
|
|
533
|
+
|
|
534
|
+
return false;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (readyState === WS_OPEN) {
|
|
539
|
+
readyState = WS_CLOSING;
|
|
540
|
+
socket.write(createWebSocketFrame(0x8, frame.payload));
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
socket.end();
|
|
544
|
+
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (frame.opcode === 0x9) {
|
|
549
|
+
if (readyState === WS_OPEN) {
|
|
550
|
+
sendFrame(0xa, frame.payload);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return true;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (frame.opcode === 0xa) {
|
|
557
|
+
return true;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (frame.opcode === 0x0) {
|
|
561
|
+
if (fragmentedOpcode === null) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (fragmentedSize + frame.payload.length > maxMessageSize) {
|
|
566
|
+
sendClose(encodeCloseCode(WS_CLOSE_MESSAGE_TOO_BIG));
|
|
567
|
+
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
fragmentedSize += frame.payload.length;
|
|
572
|
+
fragmentedPayload.push(frame.payload);
|
|
573
|
+
|
|
574
|
+
if (frame.fin) {
|
|
575
|
+
const payload = Buffer.concat(fragmentedPayload);
|
|
576
|
+
const opcode = fragmentedOpcode;
|
|
577
|
+
|
|
578
|
+
fragmentedOpcode = null;
|
|
579
|
+
fragmentedPayload = [];
|
|
580
|
+
fragmentedSize = 0;
|
|
581
|
+
|
|
582
|
+
return emitMessage(opcode, payload);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return true;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
if (fragmentedOpcode !== null) {
|
|
589
|
+
return false;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
if (frame.fin) {
|
|
593
|
+
return emitMessage(frame.opcode, frame.payload);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (frame.payload.length > maxMessageSize) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
fragmentedOpcode = frame.opcode;
|
|
601
|
+
fragmentedPayload = [frame.payload];
|
|
602
|
+
fragmentedSize = frame.payload.length;
|
|
603
|
+
|
|
604
|
+
return true;
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Parses and dispatches every complete frame currently available in the
|
|
609
|
+
* buffer.
|
|
610
|
+
*
|
|
611
|
+
* Sends the appropriate close frame when a frame violates the WebSocket
|
|
612
|
+
* protocol. Protocol violations default to `1002`; specific violations may
|
|
613
|
+
* carry a different close code through the thrown error.
|
|
614
|
+
*/
|
|
615
|
+
const processBuffer = () => {
|
|
616
|
+
/** @type {WebSocketFrame|null} */
|
|
617
|
+
let frame;
|
|
618
|
+
|
|
619
|
+
while (readyState !== WS_CLOSED) {
|
|
620
|
+
try {
|
|
621
|
+
frame = parseWebSocketFrame(bufferQueue, maxMessageSize);
|
|
622
|
+
} catch (error) {
|
|
623
|
+
const closeCode =
|
|
624
|
+
error && typeof error === 'object' && 'code' in error && typeof error.code === 'number'
|
|
625
|
+
? error.code
|
|
626
|
+
: WS_CLOSE_PROTOCOL_ERROR;
|
|
627
|
+
|
|
628
|
+
sendClose(encodeCloseCode(closeCode));
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (frame === null) {
|
|
633
|
+
break;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
bufferQueue = bufferQueue.subarray(frame.length);
|
|
637
|
+
|
|
638
|
+
if (!handleFrame(frame)) {
|
|
639
|
+
if (readyState === WS_OPEN) {
|
|
640
|
+
sendClose(encodeCloseCode(WS_CLOSE_PROTOCOL_ERROR));
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
break;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
|
|
648
|
+
socket.on('data', chunk => {
|
|
649
|
+
bufferQueue = Buffer.concat([bufferQueue, Buffer.from(chunk)]);
|
|
650
|
+
processBuffer();
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
socket.on('close', () => {
|
|
654
|
+
readyState = WS_CLOSED;
|
|
655
|
+
|
|
656
|
+
closeListeners.forEach(listener => listener());
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
socket.on('error', error => {
|
|
660
|
+
errorListeners.forEach(listener => listener(error));
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
if (head && head.length > 0) {
|
|
664
|
+
process.nextTick(() => {
|
|
665
|
+
bufferQueue = Buffer.concat([bufferQueue, head]);
|
|
666
|
+
processBuffer();
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return {
|
|
671
|
+
get readyState() {
|
|
672
|
+
return readyState;
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
send(data) {
|
|
676
|
+
if (typeof data === 'string') {
|
|
677
|
+
sendFrame(0x1, Buffer.from(data));
|
|
678
|
+
} else {
|
|
679
|
+
sendFrame(0x2, Buffer.from(data));
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
/**
|
|
684
|
+
* Registers a listener for incoming WebSocket messages.
|
|
685
|
+
*
|
|
686
|
+
* @overload
|
|
687
|
+
* @param {'message'} event - Event name.
|
|
688
|
+
* @param {WebSocketMessageListener} listener - Listener invoked for incoming messages.
|
|
689
|
+
* @returns {void}
|
|
690
|
+
*/
|
|
691
|
+
/**
|
|
692
|
+
* Registers a listener for WebSocket close events.
|
|
693
|
+
*
|
|
694
|
+
* @overload
|
|
695
|
+
* @param {'close'} event - Event name.
|
|
696
|
+
* @param {WebSocketCloseListener} listener - Listener invoked when the connection closes.
|
|
697
|
+
* @returns {void}
|
|
698
|
+
*/
|
|
699
|
+
/**
|
|
700
|
+
* Registers a listener for WebSocket error events.
|
|
701
|
+
*
|
|
702
|
+
* @overload
|
|
703
|
+
* @param {'error'} event - Event name.
|
|
704
|
+
* @param {WebSocketErrorListener} listener - Listener invoked when an error occurs.
|
|
705
|
+
* @returns {void}
|
|
706
|
+
*/
|
|
707
|
+
/**
|
|
708
|
+
* Registers a listener for a WebSocket event.
|
|
709
|
+
*
|
|
710
|
+
* @param {'message'|'close'|'error'} event - Event name.
|
|
711
|
+
* @param {WebSocketMessageListener|WebSocketCloseListener|WebSocketErrorListener} listener - Listener invoked for the event.
|
|
712
|
+
* @returns {void}
|
|
713
|
+
*/
|
|
714
|
+
on(event, listener) {
|
|
715
|
+
if (event === 'message') {
|
|
716
|
+
messageListeners.push(/** @type {WebSocketMessageListener} */ (listener));
|
|
717
|
+
} else if (event === 'close') {
|
|
718
|
+
closeListeners.push(/** @type {WebSocketCloseListener} */ (listener));
|
|
719
|
+
} else if (event === 'error') {
|
|
720
|
+
errorListeners.push(/** @type {WebSocketErrorListener} */ (listener));
|
|
721
|
+
}
|
|
722
|
+
},
|
|
723
|
+
|
|
724
|
+
close() {
|
|
725
|
+
if (readyState === WS_OPEN) {
|
|
726
|
+
sendClose(encodeCloseCode(WS_CLOSE_NORMAL));
|
|
727
|
+
} else {
|
|
728
|
+
socket.end();
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// ━━ MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
735
|
+
/**
|
|
736
|
+
* Creates a WebSocket upgrade request for an HTTP/1.1 upgrade.
|
|
737
|
+
*
|
|
738
|
+
* The returned `accept` method performs the RFC 6455 handshake and resolves to
|
|
739
|
+
* an established WebSocket connection. When the client requests subprotocols,
|
|
740
|
+
* the caller may pass a `selectedProtocol` that must be both supported by the
|
|
741
|
+
* server and requested by the client.
|
|
742
|
+
*
|
|
743
|
+
* @param {WebSocketUpgradeRequest} request - HTTP upgrade request.
|
|
744
|
+
* @param {WebSocketSocket} socket - Underlying TCP socket.
|
|
745
|
+
* @param {Buffer} head - Bytes already read during the upgrade.
|
|
746
|
+
* @param {WebSocketUpgradeOptions} [options] - Upgrade options.
|
|
747
|
+
* @returns {WebSocketOpenRequest} WebSocket upgrade request.
|
|
748
|
+
* @example
|
|
749
|
+
* ```js
|
|
750
|
+
* const openRequest = createWebSocketOpenRequest(request, socket, head, {
|
|
751
|
+
* maxMessageSize: 1024 * 1024,
|
|
752
|
+
* supportedProtocols: ['chat', 'json'],
|
|
753
|
+
* });
|
|
754
|
+
*
|
|
755
|
+
* openRequest.accept('chat');
|
|
756
|
+
* ```
|
|
757
|
+
*/
|
|
758
|
+
function createWebSocketOpenRequest(request, socket, head, options = {}) {
|
|
759
|
+
const upgradeHeader = request.headers.upgrade;
|
|
760
|
+
const connectionHeader = request.headers.connection;
|
|
761
|
+
const secWebSocketKey = request.headers['sec-websocket-key'];
|
|
762
|
+
const secWebSocketVersion = request.headers['sec-websocket-version'];
|
|
763
|
+
const secWebSocketProtocol = request.headers['sec-websocket-protocol'];
|
|
764
|
+
|
|
765
|
+
const maxMessageSize = options.maxMessageSize ?? DEFAULT_MAX_MESSAGE_SIZE;
|
|
766
|
+
const supportedProtocols = options.supportedProtocols ?? [];
|
|
767
|
+
|
|
768
|
+
return {
|
|
769
|
+
url: request.url || '/',
|
|
770
|
+
headers: request.headers,
|
|
771
|
+
|
|
772
|
+
accept(selectedProtocol) {
|
|
773
|
+
if (typeof upgradeHeader !== 'string' || upgradeHeader.toLowerCase() !== 'websocket') {
|
|
774
|
+
throw new Error("Missing or invalid 'Upgrade' header for WebSocket handshake.");
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (typeof connectionHeader !== 'string') {
|
|
778
|
+
throw new Error("Missing 'Connection' header for WebSocket handshake.");
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const connectionTokens = connectionHeader
|
|
782
|
+
.toLowerCase()
|
|
783
|
+
.split(',')
|
|
784
|
+
.map(token => token.trim());
|
|
785
|
+
|
|
786
|
+
if (!connectionTokens.includes('upgrade')) {
|
|
787
|
+
throw new Error("Missing 'Upgrade' token in 'Connection' header for WebSocket handshake.");
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
if (secWebSocketVersion !== String(WS_VERSION)) {
|
|
791
|
+
throw new Error('Unsupported WebSocket version.');
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
if (typeof secWebSocketKey !== 'string' || secWebSocketKey.length === 0) {
|
|
795
|
+
throw new Error("Missing 'Sec-WebSocket-Key' header for WebSocket handshake.");
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
if (selectedProtocol) {
|
|
799
|
+
if (!supportedProtocols.includes(selectedProtocol)) {
|
|
800
|
+
throw new Error(`Unsupported WebSocket subprotocol: ${selectedProtocol}.`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const clientProtocols =
|
|
804
|
+
typeof secWebSocketProtocol === 'string'
|
|
805
|
+
? secWebSocketProtocol.split(',').map(protocol => protocol.trim())
|
|
806
|
+
: [];
|
|
807
|
+
|
|
808
|
+
if (!clientProtocols.includes(selectedProtocol)) {
|
|
809
|
+
throw new Error(`Client did not request the selected subprotocol: ${selectedProtocol}.`);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const hash = crypto
|
|
814
|
+
.createHash('sha1')
|
|
815
|
+
.update(secWebSocketKey + WS_MAGIC_STRING)
|
|
816
|
+
.digest('base64');
|
|
817
|
+
|
|
818
|
+
const responseHeaders = [
|
|
819
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
820
|
+
'Upgrade: websocket',
|
|
821
|
+
'Connection: Upgrade',
|
|
822
|
+
`Sec-WebSocket-Accept: ${hash}`,
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
if (selectedProtocol) {
|
|
826
|
+
responseHeaders.push(`Sec-WebSocket-Protocol: ${selectedProtocol}`);
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
responseHeaders.push('\r\n');
|
|
830
|
+
socket.write(responseHeaders.join('\r\n'));
|
|
831
|
+
|
|
832
|
+
return createWebSocketConnection(socket, head, maxMessageSize);
|
|
833
|
+
},
|
|
834
|
+
|
|
835
|
+
reject(code = 400, message = 'Bad Request') {
|
|
836
|
+
socket.write(`HTTP/1.1 ${code} ${message}\r\n\r\n`);
|
|
837
|
+
socket.destroy();
|
|
838
|
+
},
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ━━ EXPORT MODULE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
843
|
+
export default createWebSocketOpenRequest;
|