@orkestrel/websocket 0.0.4 → 0.0.6
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/dist/src/server/index.cjs +159 -58
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +72 -9
- package/dist/src/server/index.d.ts +72 -9
- package/dist/src/server/index.js +155 -59
- package/dist/src/server/index.js.map +1 -1
- package/package.json +15 -14
|
@@ -93,6 +93,61 @@ export declare function encodeWebSocketFrame(opcode: number, payload: Buffer | s
|
|
|
93
93
|
*/
|
|
94
94
|
export declare function isCloseCode(code: number): boolean;
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* Returns `undefined` until the complete length prefix is buffered. The 16-bit form
|
|
101
|
+
* is canonical only for lengths at least 126; the 64-bit form only for lengths at
|
|
102
|
+
* least 65,536 and with its most-significant bit clear (RFC 6455 §5.2).
|
|
103
|
+
*
|
|
104
|
+
* @param buffer - The accumulation buffer containing the next frame header
|
|
105
|
+
* @returns Its canonicality, or `undefined` while the length prefix is incomplete
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* if (isWebSocketFrameCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function isWebSocketFrameCanonical(buffer: Buffer): boolean | undefined;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
|
|
119
|
+
* in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
|
|
120
|
+
* malformed or non-canonical encodings return `false`; nothing is thrown.
|
|
121
|
+
*
|
|
122
|
+
* @param key - The proposed `Sec-WebSocket-Key` header value
|
|
123
|
+
* @returns `true` when `key` is the canonical base64 encoding of 16 bytes
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const key = request.headers['sec-websocket-key']
|
|
128
|
+
* if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export declare function isWebSocketKey(key: string): boolean;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether a value is one valid WebSocket subprotocol token.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
|
|
138
|
+
* control characters are rejected, preventing an untrusted value from injecting a
|
|
139
|
+
* second handshake header.
|
|
140
|
+
*
|
|
141
|
+
* @param protocol - The negotiated subprotocol to validate
|
|
142
|
+
* @returns `true` when `protocol` is one non-empty HTTP token
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* if (!isWebSocketProtocol(protocol)) throw new RangeError('invalid protocol')
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare function isWebSocketProtocol(protocol: string): boolean;
|
|
150
|
+
|
|
96
151
|
/**
|
|
97
152
|
* Read the declared payload length off the front of a buffer, without buffering or
|
|
98
153
|
* reading the payload itself.
|
|
@@ -131,8 +186,9 @@ export declare function measureWebSocketFrame(buffer: Buffer): number | undefine
|
|
|
131
186
|
* socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
|
|
132
187
|
* frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
|
|
133
188
|
* isolates a throwing listener and routes the error to its own `error` handler (the `error`
|
|
134
|
-
* option) — the socket never crashes.
|
|
135
|
-
*
|
|
189
|
+
* option) — the socket never crashes. An underlying socket error emits the domain
|
|
190
|
+
* `error` event and terminates the wrapper. The untyped socket `data` is narrowed to a
|
|
191
|
+
* `Buffer` with a guard, never an assertion (AGENTS §14).
|
|
136
192
|
*/
|
|
137
193
|
export declare class NodeWebSocket implements NodeWebSocketInterface {
|
|
138
194
|
#private;
|
|
@@ -152,18 +208,19 @@ export declare class NodeWebSocket implements NodeWebSocketInterface {
|
|
|
152
208
|
* `open` — the handshake completed and the socket is ready. `message` — a text frame
|
|
153
209
|
* arrived (its decoded UTF-8 string). `close` — the connection ended (its
|
|
154
210
|
* {@link WebSocketClose} metadata). `error` — the underlying socket faulted (a DOMAIN
|
|
155
|
-
* event). `ping` / `pong` — a control frame arrived
|
|
211
|
+
* event and then terminates the wrapper). `ping` / `pong` — a control frame arrived
|
|
212
|
+
* (a ping is auto-answered with a pong).
|
|
156
213
|
* Listener isolation is the emitter's (AGENTS §13): a listener throw is routed to the
|
|
157
214
|
* emitter's `error` handler (the `error` option), never onto this map, so a buggy observer
|
|
158
215
|
* never breaks the socket.
|
|
159
216
|
*/
|
|
160
217
|
export declare type NodeWebSocketEventMap = {
|
|
161
|
-
open: [];
|
|
162
|
-
message: [message: string];
|
|
163
|
-
close: [code: number | undefined, reason: string | undefined];
|
|
164
|
-
error: [error: unknown];
|
|
165
|
-
ping: [];
|
|
166
|
-
pong: [];
|
|
218
|
+
readonly open: readonly [];
|
|
219
|
+
readonly message: readonly [message: string];
|
|
220
|
+
readonly close: readonly [code: number | undefined, reason: string | undefined];
|
|
221
|
+
readonly error: readonly [error: unknown];
|
|
222
|
+
readonly ping: readonly [];
|
|
223
|
+
readonly pong: readonly [];
|
|
167
224
|
};
|
|
168
225
|
|
|
169
226
|
/**
|
|
@@ -271,6 +328,9 @@ export declare const WEBSOCKET_CLOSE_NORMAL = 1000;
|
|
|
271
328
|
/** Protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */
|
|
272
329
|
export declare const WEBSOCKET_CLOSE_PROTOCOL = 1002;
|
|
273
330
|
|
|
331
|
+
/** The maximum UTF-8 close-reason length after the two-byte status code. */
|
|
332
|
+
export declare const WEBSOCKET_CLOSE_REASON_MAXLEN: number;
|
|
333
|
+
|
|
274
334
|
/** The default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */
|
|
275
335
|
export declare const WEBSOCKET_CLOSE_TIMEOUT_MS = 30000;
|
|
276
336
|
|
|
@@ -305,6 +365,9 @@ export declare const WEBSOCKET_OPCODE_BINARY = 2;
|
|
|
305
365
|
/** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
|
|
306
366
|
export declare const WEBSOCKET_OPCODE_CLOSE = 8;
|
|
307
367
|
|
|
368
|
+
/** Continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
|
|
369
|
+
export declare const WEBSOCKET_OPCODE_CONTINUATION = 0;
|
|
370
|
+
|
|
308
371
|
/** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
|
|
309
372
|
export declare const WEBSOCKET_OPCODE_PING = 9;
|
|
310
373
|
|
|
@@ -93,6 +93,61 @@ export declare function encodeWebSocketFrame(opcode: number, payload: Buffer | s
|
|
|
93
93
|
*/
|
|
94
94
|
export declare function isCloseCode(code: number): boolean;
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
|
|
98
|
+
*
|
|
99
|
+
* @remarks
|
|
100
|
+
* Returns `undefined` until the complete length prefix is buffered. The 16-bit form
|
|
101
|
+
* is canonical only for lengths at least 126; the 64-bit form only for lengths at
|
|
102
|
+
* least 65,536 and with its most-significant bit clear (RFC 6455 §5.2).
|
|
103
|
+
*
|
|
104
|
+
* @param buffer - The accumulation buffer containing the next frame header
|
|
105
|
+
* @returns Its canonicality, or `undefined` while the length prefix is incomplete
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* ```ts
|
|
109
|
+
* if (isWebSocketFrameCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
export declare function isWebSocketFrameCanonical(buffer: Buffer): boolean | undefined;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
|
|
116
|
+
*
|
|
117
|
+
* @remarks
|
|
118
|
+
* A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
|
|
119
|
+
* in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
|
|
120
|
+
* malformed or non-canonical encodings return `false`; nothing is thrown.
|
|
121
|
+
*
|
|
122
|
+
* @param key - The proposed `Sec-WebSocket-Key` header value
|
|
123
|
+
* @returns `true` when `key` is the canonical base64 encoding of 16 bytes
|
|
124
|
+
*
|
|
125
|
+
* @example
|
|
126
|
+
* ```ts
|
|
127
|
+
* const key = request.headers['sec-websocket-key']
|
|
128
|
+
* if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
|
|
129
|
+
* ```
|
|
130
|
+
*/
|
|
131
|
+
export declare function isWebSocketKey(key: string): boolean;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Whether a value is one valid WebSocket subprotocol token.
|
|
135
|
+
*
|
|
136
|
+
* @remarks
|
|
137
|
+
* Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
|
|
138
|
+
* control characters are rejected, preventing an untrusted value from injecting a
|
|
139
|
+
* second handshake header.
|
|
140
|
+
*
|
|
141
|
+
* @param protocol - The negotiated subprotocol to validate
|
|
142
|
+
* @returns `true` when `protocol` is one non-empty HTTP token
|
|
143
|
+
*
|
|
144
|
+
* @example
|
|
145
|
+
* ```ts
|
|
146
|
+
* if (!isWebSocketProtocol(protocol)) throw new RangeError('invalid protocol')
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
export declare function isWebSocketProtocol(protocol: string): boolean;
|
|
150
|
+
|
|
96
151
|
/**
|
|
97
152
|
* Read the declared payload length off the front of a buffer, without buffering or
|
|
98
153
|
* reading the payload itself.
|
|
@@ -131,8 +186,9 @@ export declare function measureWebSocketFrame(buffer: Buffer): number | undefine
|
|
|
131
186
|
* socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
|
|
132
187
|
* frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
|
|
133
188
|
* isolates a throwing listener and routes the error to its own `error` handler (the `error`
|
|
134
|
-
* option) — the socket never crashes.
|
|
135
|
-
*
|
|
189
|
+
* option) — the socket never crashes. An underlying socket error emits the domain
|
|
190
|
+
* `error` event and terminates the wrapper. The untyped socket `data` is narrowed to a
|
|
191
|
+
* `Buffer` with a guard, never an assertion (AGENTS §14).
|
|
136
192
|
*/
|
|
137
193
|
export declare class NodeWebSocket implements NodeWebSocketInterface {
|
|
138
194
|
#private;
|
|
@@ -152,18 +208,19 @@ export declare class NodeWebSocket implements NodeWebSocketInterface {
|
|
|
152
208
|
* `open` — the handshake completed and the socket is ready. `message` — a text frame
|
|
153
209
|
* arrived (its decoded UTF-8 string). `close` — the connection ended (its
|
|
154
210
|
* {@link WebSocketClose} metadata). `error` — the underlying socket faulted (a DOMAIN
|
|
155
|
-
* event). `ping` / `pong` — a control frame arrived
|
|
211
|
+
* event and then terminates the wrapper). `ping` / `pong` — a control frame arrived
|
|
212
|
+
* (a ping is auto-answered with a pong).
|
|
156
213
|
* Listener isolation is the emitter's (AGENTS §13): a listener throw is routed to the
|
|
157
214
|
* emitter's `error` handler (the `error` option), never onto this map, so a buggy observer
|
|
158
215
|
* never breaks the socket.
|
|
159
216
|
*/
|
|
160
217
|
export declare type NodeWebSocketEventMap = {
|
|
161
|
-
open: [];
|
|
162
|
-
message: [message: string];
|
|
163
|
-
close: [code: number | undefined, reason: string | undefined];
|
|
164
|
-
error: [error: unknown];
|
|
165
|
-
ping: [];
|
|
166
|
-
pong: [];
|
|
218
|
+
readonly open: readonly [];
|
|
219
|
+
readonly message: readonly [message: string];
|
|
220
|
+
readonly close: readonly [code: number | undefined, reason: string | undefined];
|
|
221
|
+
readonly error: readonly [error: unknown];
|
|
222
|
+
readonly ping: readonly [];
|
|
223
|
+
readonly pong: readonly [];
|
|
167
224
|
};
|
|
168
225
|
|
|
169
226
|
/**
|
|
@@ -271,6 +328,9 @@ export declare const WEBSOCKET_CLOSE_NORMAL = 1000;
|
|
|
271
328
|
/** Protocol-error status code (RFC 6455 §7.4.1) — a framing/state rule was violated. */
|
|
272
329
|
export declare const WEBSOCKET_CLOSE_PROTOCOL = 1002;
|
|
273
330
|
|
|
331
|
+
/** The maximum UTF-8 close-reason length after the two-byte status code. */
|
|
332
|
+
export declare const WEBSOCKET_CLOSE_REASON_MAXLEN: number;
|
|
333
|
+
|
|
274
334
|
/** The default close-handshake timeout in milliseconds — how long `close()` waits for the peer's echo before tearing the socket down. */
|
|
275
335
|
export declare const WEBSOCKET_CLOSE_TIMEOUT_MS = 30000;
|
|
276
336
|
|
|
@@ -305,6 +365,9 @@ export declare const WEBSOCKET_OPCODE_BINARY = 2;
|
|
|
305
365
|
/** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
|
|
306
366
|
export declare const WEBSOCKET_OPCODE_CLOSE = 8;
|
|
307
367
|
|
|
368
|
+
/** Continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
|
|
369
|
+
export declare const WEBSOCKET_OPCODE_CONTINUATION = 0;
|
|
370
|
+
|
|
308
371
|
/** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
|
|
309
372
|
export declare const WEBSOCKET_OPCODE_PING = 9;
|
|
310
373
|
|
package/dist/src/server/index.js
CHANGED
|
@@ -16,6 +16,8 @@ var WEBSOCKET_VERSION = "13";
|
|
|
16
16
|
var WEBSOCKET_OPCODE_TEXT = 1;
|
|
17
17
|
/** Binary frame opcode — a raw byte payload (RFC 6455 §5.6). */
|
|
18
18
|
var WEBSOCKET_OPCODE_BINARY = 2;
|
|
19
|
+
/** Continuation frame opcode — the next fragment of an open data message (RFC 6455 §5.4). */
|
|
20
|
+
var WEBSOCKET_OPCODE_CONTINUATION = 0;
|
|
19
21
|
/** Close frame opcode — a control frame ending the connection (RFC 6455 §5.5.1). */
|
|
20
22
|
var WEBSOCKET_OPCODE_CLOSE = 8;
|
|
21
23
|
/** Ping frame opcode — a control frame the peer must answer with a pong (RFC 6455 §5.5.2). */
|
|
@@ -48,6 +50,8 @@ var WEBSOCKET_CLOSE_TIMEOUT_MS = 3e4;
|
|
|
48
50
|
var WEBSOCKET_FAIL_TIMEOUT_MS = 1e3;
|
|
49
51
|
/** The maximum control-frame payload length in bytes (RFC 6455 §5.5). */
|
|
50
52
|
var WEBSOCKET_CONTROL_MAXLEN = 125;
|
|
53
|
+
/** The maximum UTF-8 close-reason length after the two-byte status code. */
|
|
54
|
+
var WEBSOCKET_CLOSE_REASON_MAXLEN = 123;
|
|
51
55
|
//#endregion
|
|
52
56
|
//#region src/server/helpers.ts
|
|
53
57
|
/**
|
|
@@ -65,6 +69,46 @@ function computeWebSocketAccept(key) {
|
|
|
65
69
|
return createHash("sha1").update(key + WEBSOCKET_GUID).digest("base64");
|
|
66
70
|
}
|
|
67
71
|
/**
|
|
72
|
+
* Whether a value is a canonical RFC 6455 `Sec-WebSocket-Key`.
|
|
73
|
+
*
|
|
74
|
+
* @remarks
|
|
75
|
+
* A valid key is exactly 16 random bytes encoded as 24 characters of base64, ending
|
|
76
|
+
* in `==` (RFC 6455 §4.1). This predicate is suitable at an HTTP upgrade boundary:
|
|
77
|
+
* malformed or non-canonical encodings return `false`; nothing is thrown.
|
|
78
|
+
*
|
|
79
|
+
* @param key - The proposed `Sec-WebSocket-Key` header value
|
|
80
|
+
* @returns `true` when `key` is the canonical base64 encoding of 16 bytes
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* ```ts
|
|
84
|
+
* const key = request.headers['sec-websocket-key']
|
|
85
|
+
* if (typeof key !== 'string' || !isWebSocketKey(key)) socket.destroy()
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
function isWebSocketKey(key) {
|
|
89
|
+
if (!/^[A-Za-z0-9+/]{22}==$/.test(key)) return false;
|
|
90
|
+
return Buffer.from(key, "base64").length === 16;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Whether a value is one valid WebSocket subprotocol token.
|
|
94
|
+
*
|
|
95
|
+
* @remarks
|
|
96
|
+
* Subprotocols use the HTTP `token` grammar. Whitespace, separators, commas, and
|
|
97
|
+
* control characters are rejected, preventing an untrusted value from injecting a
|
|
98
|
+
* second handshake header.
|
|
99
|
+
*
|
|
100
|
+
* @param protocol - The negotiated subprotocol to validate
|
|
101
|
+
* @returns `true` when `protocol` is one non-empty HTTP token
|
|
102
|
+
*
|
|
103
|
+
* @example
|
|
104
|
+
* ```ts
|
|
105
|
+
* if (!isWebSocketProtocol(protocol)) throw new RangeError('invalid protocol')
|
|
106
|
+
* ```
|
|
107
|
+
*/
|
|
108
|
+
function isWebSocketProtocol(protocol) {
|
|
109
|
+
return /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/.test(protocol);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
68
112
|
* Decode a single RFC 6455 frame from the front of a buffer.
|
|
69
113
|
*
|
|
70
114
|
* @remarks
|
|
@@ -84,8 +128,8 @@ function computeWebSocketAccept(key) {
|
|
|
84
128
|
*/
|
|
85
129
|
function parseWebSocketFrame(buffer) {
|
|
86
130
|
if (buffer.length < 2) return void 0;
|
|
87
|
-
const firstByte = buffer
|
|
88
|
-
const secondByte = buffer
|
|
131
|
+
const firstByte = buffer.readUInt8(0);
|
|
132
|
+
const secondByte = buffer.readUInt8(1);
|
|
89
133
|
const fin = (firstByte & 128) !== 0;
|
|
90
134
|
const rsv = (firstByte & 112) >> 4;
|
|
91
135
|
const opcode = firstByte & 15;
|
|
@@ -112,7 +156,7 @@ function parseWebSocketFrame(buffer) {
|
|
|
112
156
|
if (buffer.length < offset + length) return void 0;
|
|
113
157
|
const payload = Buffer.alloc(length);
|
|
114
158
|
buffer.copy(payload, 0, offset, offset + length);
|
|
115
|
-
if (mask !== void 0) for (let index = 0; index < length; index += 1) payload[index] = (
|
|
159
|
+
if (mask !== void 0) for (let index = 0; index < length; index += 1) payload[index] = payload.readUInt8(index) ^ mask.readUInt8(index % 4);
|
|
116
160
|
return {
|
|
117
161
|
fin,
|
|
118
162
|
opcode,
|
|
@@ -144,7 +188,7 @@ function parseWebSocketFrame(buffer) {
|
|
|
144
188
|
*/
|
|
145
189
|
function measureWebSocketFrame(buffer) {
|
|
146
190
|
if (buffer.length < 2) return void 0;
|
|
147
|
-
let length = (
|
|
191
|
+
let length = buffer.readUInt8(1) & 127;
|
|
148
192
|
const offset = 2;
|
|
149
193
|
if (length === 126) {
|
|
150
194
|
if (buffer.length < 4) return void 0;
|
|
@@ -158,6 +202,36 @@ function measureWebSocketFrame(buffer) {
|
|
|
158
202
|
return length;
|
|
159
203
|
}
|
|
160
204
|
/**
|
|
205
|
+
* Whether the next frame uses the shortest valid RFC 6455 payload-length encoding.
|
|
206
|
+
*
|
|
207
|
+
* @remarks
|
|
208
|
+
* Returns `undefined` until the complete length prefix is buffered. The 16-bit form
|
|
209
|
+
* is canonical only for lengths at least 126; the 64-bit form only for lengths at
|
|
210
|
+
* least 65,536 and with its most-significant bit clear (RFC 6455 §5.2).
|
|
211
|
+
*
|
|
212
|
+
* @param buffer - The accumulation buffer containing the next frame header
|
|
213
|
+
* @returns Its canonicality, or `undefined` while the length prefix is incomplete
|
|
214
|
+
*
|
|
215
|
+
* @example
|
|
216
|
+
* ```ts
|
|
217
|
+
* if (isWebSocketFrameCanonical(buffer) === false) fail(WEBSOCKET_CLOSE_PROTOCOL)
|
|
218
|
+
* ```
|
|
219
|
+
*/
|
|
220
|
+
function isWebSocketFrameCanonical(buffer) {
|
|
221
|
+
if (buffer.length < 2) return void 0;
|
|
222
|
+
const lengthCode = buffer.readUInt8(1) & 127;
|
|
223
|
+
if (lengthCode < 126) return true;
|
|
224
|
+
if (lengthCode === 126) {
|
|
225
|
+
if (buffer.length < 4) return void 0;
|
|
226
|
+
return buffer.readUInt16BE(2) >= 126;
|
|
227
|
+
}
|
|
228
|
+
if (buffer.length < 10) return void 0;
|
|
229
|
+
const high = buffer.readUInt32BE(2);
|
|
230
|
+
const low = buffer.readUInt32BE(6);
|
|
231
|
+
if ((high & 2147483648) !== 0) return false;
|
|
232
|
+
return high > 0 || low >= 65536;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
161
235
|
* Decode a byte sequence as strict UTF-8, or signal it is malformed.
|
|
162
236
|
*
|
|
163
237
|
* @remarks
|
|
@@ -203,6 +277,7 @@ function parseUTF8(bytes) {
|
|
|
203
277
|
* ```
|
|
204
278
|
*/
|
|
205
279
|
function isCloseCode(code) {
|
|
280
|
+
if (!Number.isInteger(code)) return false;
|
|
206
281
|
if (code >= 1e3 && code <= 1003) return true;
|
|
207
282
|
if (code >= 1007 && code <= 1014) return true;
|
|
208
283
|
if (code >= 3e3 && code <= 4999) return true;
|
|
@@ -228,6 +303,9 @@ function isCloseCode(code) {
|
|
|
228
303
|
* @returns The complete frame as wire bytes
|
|
229
304
|
*/
|
|
230
305
|
function encodeWebSocketFrame(opcode, payload, options) {
|
|
306
|
+
if (!Number.isInteger(opcode) || opcode < 0 || opcode > 15) throw new RangeError("opcode must be an integer between 0 and 15");
|
|
307
|
+
if (options?.mask !== void 0 && options.mask.length !== 4) throw new RangeError("mask must contain exactly 4 bytes");
|
|
308
|
+
if (options?.mask !== void 0 && options.masked !== true) throw new RangeError("mask requires masked: true");
|
|
231
309
|
const body = typeof payload === "string" ? Buffer.from(payload, "utf-8") : payload;
|
|
232
310
|
const length = body.length;
|
|
233
311
|
const masked = options?.masked === true;
|
|
@@ -248,7 +326,7 @@ function encodeWebSocketFrame(opcode, payload, options) {
|
|
|
248
326
|
if (mask === void 0) return Buffer.concat([header, body]);
|
|
249
327
|
mask.copy(header, header.length - 4);
|
|
250
328
|
const maskedBody = Buffer.alloc(length);
|
|
251
|
-
for (let index = 0; index < length; index += 1) maskedBody[index] = (
|
|
329
|
+
for (let index = 0; index < length; index += 1) maskedBody[index] = body.readUInt8(index) ^ mask.readUInt8(index % 4);
|
|
252
330
|
return Buffer.concat([header, maskedBody]);
|
|
253
331
|
}
|
|
254
332
|
//#endregion
|
|
@@ -269,72 +347,71 @@ function encodeWebSocketFrame(opcode, payload, options) {
|
|
|
269
347
|
* socket, emitting `close`. `send` writes a text frame, `ping` a ping, `close` a close
|
|
270
348
|
* frame; `destroy` tears down immediately. It owns a typed `#emitter` (AGENTS §13) that
|
|
271
349
|
* isolates a throwing listener and routes the error to its own `error` handler (the `error`
|
|
272
|
-
* option) — the socket never crashes.
|
|
273
|
-
*
|
|
350
|
+
* option) — the socket never crashes. An underlying socket error emits the domain
|
|
351
|
+
* `error` event and terminates the wrapper. The untyped socket `data` is narrowed to a
|
|
352
|
+
* `Buffer` with a guard, never an assertion (AGENTS §14).
|
|
274
353
|
*/
|
|
275
354
|
var NodeWebSocket = class {
|
|
276
355
|
#emitter;
|
|
277
356
|
#socket;
|
|
278
|
-
#protocol;
|
|
279
357
|
#masked;
|
|
280
358
|
#payload;
|
|
281
359
|
#timeout;
|
|
282
|
-
#requireMask;
|
|
283
360
|
#signal;
|
|
361
|
+
#dataListener;
|
|
362
|
+
#closeListener;
|
|
363
|
+
#errorListener;
|
|
364
|
+
#abortListener;
|
|
284
365
|
#buffer = Buffer.alloc(0);
|
|
285
366
|
#readyState = 0;
|
|
286
|
-
#code
|
|
287
|
-
#reason
|
|
367
|
+
#code;
|
|
368
|
+
#reason;
|
|
288
369
|
#fragments = [];
|
|
289
|
-
#messageOpcode
|
|
370
|
+
#messageOpcode;
|
|
290
371
|
#fragmentBytes = 0;
|
|
291
|
-
#closeTimer
|
|
372
|
+
#closeTimer;
|
|
292
373
|
#destroyed = false;
|
|
293
374
|
#detached = false;
|
|
294
|
-
#onData = (chunk) => {
|
|
295
|
-
if (this.#readyState === 3) return;
|
|
296
|
-
const bytes = this.#bytes(chunk);
|
|
297
|
-
if (bytes === void 0) return;
|
|
298
|
-
this.#ingest(bytes);
|
|
299
|
-
};
|
|
300
|
-
#onClose = () => {
|
|
301
|
-
this.#finish();
|
|
302
|
-
};
|
|
303
|
-
#onError = (error) => {
|
|
304
|
-
this.#emitter.emit("error", error);
|
|
305
|
-
};
|
|
306
|
-
#onDetachedError = () => void 0;
|
|
307
|
-
#onAbort = () => {
|
|
308
|
-
this.destroy();
|
|
309
|
-
};
|
|
310
375
|
constructor(options) {
|
|
376
|
+
const payload = options.payload ?? 104857600;
|
|
377
|
+
if (!Number.isSafeInteger(payload) || payload < 0) throw new RangeError("payload must be a non-negative safe integer");
|
|
378
|
+
const timeout = options.timeout ?? 3e4;
|
|
379
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) throw new RangeError("timeout must be a non-negative safe integer");
|
|
380
|
+
if (options.key !== void 0 && !isWebSocketKey(options.key)) throw new RangeError("key must be the canonical base64 encoding of 16 bytes");
|
|
381
|
+
if (options.protocol !== void 0 && !isWebSocketProtocol(options.protocol)) throw new RangeError("protocol must be a valid WebSocket subprotocol token");
|
|
382
|
+
if (options.protocol !== void 0 && options.key === void 0) throw new RangeError("protocol requires a server key");
|
|
311
383
|
this.#emitter = new Emitter({
|
|
312
|
-
on: options.on,
|
|
313
|
-
error: options.error
|
|
384
|
+
...options.on === void 0 ? {} : { on: options.on },
|
|
385
|
+
...options.error === void 0 ? {} : { error: options.error }
|
|
314
386
|
});
|
|
315
387
|
this.#socket = options.socket;
|
|
316
|
-
this.#protocol = options.protocol;
|
|
317
388
|
this.#masked = options.key === void 0;
|
|
318
|
-
this.#payload =
|
|
319
|
-
this.#timeout =
|
|
320
|
-
this.#requireMask = !this.#masked;
|
|
389
|
+
this.#payload = payload;
|
|
390
|
+
this.#timeout = timeout;
|
|
321
391
|
this.#signal = options.signal;
|
|
392
|
+
this.#dataListener = this.#handleData.bind(this);
|
|
393
|
+
this.#closeListener = this.#finish.bind(this);
|
|
394
|
+
this.#errorListener = this.#handleError.bind(this);
|
|
395
|
+
this.#abortListener = this.destroy.bind(this);
|
|
322
396
|
if (options.key !== void 0) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
Upgrade: websocket
|
|
326
|
-
Connection: Upgrade
|
|
327
|
-
Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}
|
|
397
|
+
const headers = [
|
|
398
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
399
|
+
"Upgrade: websocket",
|
|
400
|
+
"Connection: Upgrade",
|
|
401
|
+
`Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}`
|
|
402
|
+
];
|
|
403
|
+
if (options.protocol !== void 0) headers.push(`Sec-WebSocket-Protocol: ${options.protocol}`);
|
|
404
|
+
this.#socket.write(`${headers.join("\r\n")}\r\n\r\n`);
|
|
328
405
|
}
|
|
329
406
|
this.#readyState = 1;
|
|
330
|
-
this.#socket.on("data", this.#
|
|
331
|
-
this.#socket.on("close", this.#
|
|
332
|
-
this.#socket.on("error", this.#
|
|
407
|
+
this.#socket.on("data", this.#dataListener);
|
|
408
|
+
this.#socket.on("close", this.#closeListener);
|
|
409
|
+
this.#socket.on("error", this.#errorListener);
|
|
333
410
|
this.#emitter.emit("open");
|
|
334
411
|
const head = options.head;
|
|
335
412
|
if (head !== void 0 && head.length > 0) this.#ingest(head);
|
|
336
413
|
if (this.#readyState !== 3) if (this.#signal?.aborted === true) this.destroy();
|
|
337
|
-
else this.#signal?.addEventListener("abort", this.#
|
|
414
|
+
else this.#signal?.addEventListener("abort", this.#abortListener, { once: true });
|
|
338
415
|
}
|
|
339
416
|
get emitter() {
|
|
340
417
|
return this.#emitter;
|
|
@@ -354,7 +431,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
354
431
|
close(code, reason) {
|
|
355
432
|
if (this.#readyState === 2 || this.#readyState === 3) return;
|
|
356
433
|
if (code !== void 0 && !isCloseCode(code)) throw new RangeError("invalid close code");
|
|
357
|
-
if (reason !== void 0 && Buffer.byteLength(reason, "utf-8") > 123) throw new RangeError(
|
|
434
|
+
if (reason !== void 0 && Buffer.byteLength(reason, "utf-8") > 123) throw new RangeError(`close reason exceeds 123 bytes`);
|
|
358
435
|
this.#readyState = 2;
|
|
359
436
|
this.#code = code ?? 1e3;
|
|
360
437
|
this.#reason = reason === void 0 || reason.length === 0 ? void 0 : reason;
|
|
@@ -367,7 +444,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
367
444
|
if (this.#destroyed) return;
|
|
368
445
|
this.#destroyed = true;
|
|
369
446
|
this.#detach();
|
|
370
|
-
this.#signal?.removeEventListener("abort", this.#
|
|
447
|
+
this.#signal?.removeEventListener("abort", this.#abortListener);
|
|
371
448
|
clearTimeout(this.#closeTimer);
|
|
372
449
|
this.#closeTimer = void 0;
|
|
373
450
|
if (!this.#socket.destroyed) this.#socket.destroy();
|
|
@@ -376,6 +453,15 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
376
453
|
}
|
|
377
454
|
#drain() {
|
|
378
455
|
for (;;) {
|
|
456
|
+
if (isWebSocketFrameCanonical(this.#buffer) === false) {
|
|
457
|
+
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const declared = measureWebSocketFrame(this.#buffer);
|
|
461
|
+
if (declared !== void 0 && declared > this.#payload) {
|
|
462
|
+
this.#fail(WEBSOCKET_CLOSE_TOOBIG);
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
379
465
|
const frame = parseWebSocketFrame(this.#buffer);
|
|
380
466
|
if (frame === void 0) return;
|
|
381
467
|
this.#buffer = this.#buffer.subarray(frame.consumed);
|
|
@@ -388,7 +474,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
388
474
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
389
475
|
return;
|
|
390
476
|
}
|
|
391
|
-
if (masked
|
|
477
|
+
if (masked === this.#masked) {
|
|
392
478
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
393
479
|
return;
|
|
394
480
|
}
|
|
@@ -479,10 +565,10 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
479
565
|
#detach() {
|
|
480
566
|
if (this.#detached) return;
|
|
481
567
|
this.#detached = true;
|
|
482
|
-
this.#socket.off("data", this.#
|
|
483
|
-
this.#socket.off("close", this.#
|
|
484
|
-
this.#socket.off("error", this.#
|
|
485
|
-
this.#socket.on("error",
|
|
568
|
+
this.#socket.off("data", this.#dataListener);
|
|
569
|
+
this.#socket.off("close", this.#closeListener);
|
|
570
|
+
this.#socket.off("error", this.#errorListener);
|
|
571
|
+
this.#socket.on("error", () => void 0);
|
|
486
572
|
}
|
|
487
573
|
#write(opcode, payload) {
|
|
488
574
|
if (this.#socket.destroyed) return;
|
|
@@ -511,7 +597,12 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
511
597
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
512
598
|
return false;
|
|
513
599
|
}
|
|
514
|
-
|
|
600
|
+
if (payload.length === 2) {
|
|
601
|
+
this.#code = code;
|
|
602
|
+
this.#reason = void 0;
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
const reason = parseUTF8(payload.subarray(2));
|
|
515
606
|
if (reason === void 0) {
|
|
516
607
|
this.#fail(WEBSOCKET_CLOSE_INVALID);
|
|
517
608
|
return false;
|
|
@@ -525,19 +616,24 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
525
616
|
this.#detach();
|
|
526
617
|
clearTimeout(this.#closeTimer);
|
|
527
618
|
this.#closeTimer = void 0;
|
|
528
|
-
this.#signal?.removeEventListener("abort", this.#
|
|
619
|
+
this.#signal?.removeEventListener("abort", this.#abortListener);
|
|
529
620
|
this.#readyState = 3;
|
|
530
621
|
this.#emitter.emit("close", this.#code, this.#reason);
|
|
531
622
|
}
|
|
532
623
|
#ingest(bytes) {
|
|
533
624
|
this.#buffer = Buffer.concat([this.#buffer, bytes]);
|
|
534
|
-
const declared = measureWebSocketFrame(this.#buffer);
|
|
535
|
-
if (declared !== void 0 && declared > this.#payload) {
|
|
536
|
-
this.#fail(WEBSOCKET_CLOSE_TOOBIG);
|
|
537
|
-
return;
|
|
538
|
-
}
|
|
539
625
|
this.#drain();
|
|
540
626
|
}
|
|
627
|
+
#handleData(chunk) {
|
|
628
|
+
if (this.#readyState === 3) return;
|
|
629
|
+
const bytes = this.#bytes(chunk);
|
|
630
|
+
if (bytes === void 0) return;
|
|
631
|
+
this.#ingest(bytes);
|
|
632
|
+
}
|
|
633
|
+
#handleError(error) {
|
|
634
|
+
this.#emitter.emit("error", error);
|
|
635
|
+
this.destroy();
|
|
636
|
+
}
|
|
541
637
|
#bytes(chunk) {
|
|
542
638
|
if (Buffer.isBuffer(chunk)) return chunk;
|
|
543
639
|
if (typeof chunk === "string") return Buffer.from(chunk, "utf-8");
|
|
@@ -580,6 +676,6 @@ function createNodeWebSocket(options) {
|
|
|
580
676
|
return new NodeWebSocket(options);
|
|
581
677
|
}
|
|
582
678
|
//#endregion
|
|
583
|
-
export { NodeWebSocket, WEBSOCKET_CLOSE_INVALID, WEBSOCKET_CLOSE_NORMAL, WEBSOCKET_CLOSE_PROTOCOL, WEBSOCKET_CLOSE_TIMEOUT_MS, WEBSOCKET_CLOSE_TOOBIG, WEBSOCKET_CLOSE_UNSUPPORTED, WEBSOCKET_CONTROL_MAXLEN, WEBSOCKET_FAIL_TIMEOUT_MS, WEBSOCKET_GUID, WEBSOCKET_MAX_PAYLOAD, WEBSOCKET_OPCODE_BINARY, WEBSOCKET_OPCODE_CLOSE, WEBSOCKET_OPCODE_PING, WEBSOCKET_OPCODE_PONG, WEBSOCKET_OPCODE_TEXT, WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_CLOSING, WEBSOCKET_READY_CONNECTING, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket, encodeWebSocketFrame, isCloseCode, measureWebSocketFrame, parseUTF8, parseWebSocketFrame };
|
|
679
|
+
export { NodeWebSocket, WEBSOCKET_CLOSE_INVALID, WEBSOCKET_CLOSE_NORMAL, WEBSOCKET_CLOSE_PROTOCOL, WEBSOCKET_CLOSE_REASON_MAXLEN, WEBSOCKET_CLOSE_TIMEOUT_MS, WEBSOCKET_CLOSE_TOOBIG, WEBSOCKET_CLOSE_UNSUPPORTED, WEBSOCKET_CONTROL_MAXLEN, WEBSOCKET_FAIL_TIMEOUT_MS, WEBSOCKET_GUID, WEBSOCKET_MAX_PAYLOAD, WEBSOCKET_OPCODE_BINARY, WEBSOCKET_OPCODE_CLOSE, WEBSOCKET_OPCODE_CONTINUATION, WEBSOCKET_OPCODE_PING, WEBSOCKET_OPCODE_PONG, WEBSOCKET_OPCODE_TEXT, WEBSOCKET_READY_CLOSED, WEBSOCKET_READY_CLOSING, WEBSOCKET_READY_CONNECTING, WEBSOCKET_READY_OPEN, WEBSOCKET_VERSION, computeWebSocketAccept, createNodeWebSocket, encodeWebSocketFrame, isCloseCode, isWebSocketFrameCanonical, isWebSocketKey, isWebSocketProtocol, measureWebSocketFrame, parseUTF8, parseWebSocketFrame };
|
|
584
680
|
|
|
585
681
|
//# sourceMappingURL=index.js.map
|