@orkestrel/websocket 0.0.4 → 0.0.5
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.
|
@@ -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,26 +347,25 @@ 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;
|
|
284
361
|
#buffer = Buffer.alloc(0);
|
|
285
362
|
#readyState = 0;
|
|
286
|
-
#code
|
|
287
|
-
#reason
|
|
363
|
+
#code;
|
|
364
|
+
#reason;
|
|
288
365
|
#fragments = [];
|
|
289
|
-
#messageOpcode
|
|
366
|
+
#messageOpcode;
|
|
290
367
|
#fragmentBytes = 0;
|
|
291
|
-
#closeTimer
|
|
368
|
+
#closeTimer;
|
|
292
369
|
#destroyed = false;
|
|
293
370
|
#detached = false;
|
|
294
371
|
#onData = (chunk) => {
|
|
@@ -302,29 +379,38 @@ var NodeWebSocket = class {
|
|
|
302
379
|
};
|
|
303
380
|
#onError = (error) => {
|
|
304
381
|
this.#emitter.emit("error", error);
|
|
382
|
+
this.destroy();
|
|
305
383
|
};
|
|
306
384
|
#onDetachedError = () => void 0;
|
|
307
385
|
#onAbort = () => {
|
|
308
386
|
this.destroy();
|
|
309
387
|
};
|
|
310
388
|
constructor(options) {
|
|
389
|
+
const payload = options.payload ?? 104857600;
|
|
390
|
+
if (!Number.isSafeInteger(payload) || payload < 0) throw new RangeError("payload must be a non-negative safe integer");
|
|
391
|
+
const timeout = options.timeout ?? 3e4;
|
|
392
|
+
if (!Number.isSafeInteger(timeout) || timeout < 0) throw new RangeError("timeout must be a non-negative safe integer");
|
|
393
|
+
if (options.key !== void 0 && !isWebSocketKey(options.key)) throw new RangeError("key must be the canonical base64 encoding of 16 bytes");
|
|
394
|
+
if (options.protocol !== void 0 && !isWebSocketProtocol(options.protocol)) throw new RangeError("protocol must be a valid WebSocket subprotocol token");
|
|
395
|
+
if (options.protocol !== void 0 && options.key === void 0) throw new RangeError("protocol requires a server key");
|
|
311
396
|
this.#emitter = new Emitter({
|
|
312
397
|
on: options.on,
|
|
313
398
|
error: options.error
|
|
314
399
|
});
|
|
315
400
|
this.#socket = options.socket;
|
|
316
|
-
this.#protocol = options.protocol;
|
|
317
401
|
this.#masked = options.key === void 0;
|
|
318
|
-
this.#payload =
|
|
319
|
-
this.#timeout =
|
|
320
|
-
this.#requireMask = !this.#masked;
|
|
402
|
+
this.#payload = payload;
|
|
403
|
+
this.#timeout = timeout;
|
|
321
404
|
this.#signal = options.signal;
|
|
322
405
|
if (options.key !== void 0) {
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
Upgrade: websocket
|
|
326
|
-
Connection: Upgrade
|
|
327
|
-
Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}
|
|
406
|
+
const headers = [
|
|
407
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
408
|
+
"Upgrade: websocket",
|
|
409
|
+
"Connection: Upgrade",
|
|
410
|
+
`Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}`
|
|
411
|
+
];
|
|
412
|
+
if (options.protocol !== void 0) headers.push(`Sec-WebSocket-Protocol: ${options.protocol}`);
|
|
413
|
+
this.#socket.write(`${headers.join("\r\n")}\r\n\r\n`);
|
|
328
414
|
}
|
|
329
415
|
this.#readyState = 1;
|
|
330
416
|
this.#socket.on("data", this.#onData);
|
|
@@ -354,7 +440,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
354
440
|
close(code, reason) {
|
|
355
441
|
if (this.#readyState === 2 || this.#readyState === 3) return;
|
|
356
442
|
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(
|
|
443
|
+
if (reason !== void 0 && Buffer.byteLength(reason, "utf-8") > 123) throw new RangeError(`close reason exceeds 123 bytes`);
|
|
358
444
|
this.#readyState = 2;
|
|
359
445
|
this.#code = code ?? 1e3;
|
|
360
446
|
this.#reason = reason === void 0 || reason.length === 0 ? void 0 : reason;
|
|
@@ -376,6 +462,15 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
376
462
|
}
|
|
377
463
|
#drain() {
|
|
378
464
|
for (;;) {
|
|
465
|
+
if (isWebSocketFrameCanonical(this.#buffer) === false) {
|
|
466
|
+
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
const declared = measureWebSocketFrame(this.#buffer);
|
|
470
|
+
if (declared !== void 0 && declared > this.#payload) {
|
|
471
|
+
this.#fail(WEBSOCKET_CLOSE_TOOBIG);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
379
474
|
const frame = parseWebSocketFrame(this.#buffer);
|
|
380
475
|
if (frame === void 0) return;
|
|
381
476
|
this.#buffer = this.#buffer.subarray(frame.consumed);
|
|
@@ -388,7 +483,7 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
388
483
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
389
484
|
return;
|
|
390
485
|
}
|
|
391
|
-
if (masked
|
|
486
|
+
if (masked === this.#masked) {
|
|
392
487
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
393
488
|
return;
|
|
394
489
|
}
|
|
@@ -511,7 +606,12 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
511
606
|
this.#fail(WEBSOCKET_CLOSE_PROTOCOL);
|
|
512
607
|
return false;
|
|
513
608
|
}
|
|
514
|
-
|
|
609
|
+
if (payload.length === 2) {
|
|
610
|
+
this.#code = code;
|
|
611
|
+
this.#reason = void 0;
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
const reason = parseUTF8(payload.subarray(2));
|
|
515
615
|
if (reason === void 0) {
|
|
516
616
|
this.#fail(WEBSOCKET_CLOSE_INVALID);
|
|
517
617
|
return false;
|
|
@@ -531,11 +631,6 @@ Sec-WebSocket-Accept: ${computeWebSocketAccept(options.key)}\r\n` + protocol + "
|
|
|
531
631
|
}
|
|
532
632
|
#ingest(bytes) {
|
|
533
633
|
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
634
|
this.#drain();
|
|
540
635
|
}
|
|
541
636
|
#bytes(chunk) {
|
|
@@ -580,6 +675,6 @@ function createNodeWebSocket(options) {
|
|
|
580
675
|
return new NodeWebSocket(options);
|
|
581
676
|
}
|
|
582
677
|
//#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 };
|
|
678
|
+
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
679
|
|
|
585
680
|
//# sourceMappingURL=index.js.map
|