stelar-time-real 2.0.4 → 3.2.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.
@@ -0,0 +1,316 @@
1
+ /**
2
+ * @stelar-time-real WebSocket Protocol (RFC 6455)
3
+ *
4
+ * Hand-crafted implementation with no external dependencies.
5
+ * Uses Node.js built-in crypto for handshake and frame masking.
6
+ */
7
+
8
+ import { createHash, randomBytes } from 'crypto';
9
+
10
+ const WS_MAGIC = '258EAFA5-E914-47DA-95CA-5AB5A7E3A741';
11
+ export const DEFAULT_MAX_WS_FRAME_SIZE = 10 * 1024 * 1024;
12
+
13
+ export const OP_CONTINUATION = 0x0;
14
+ export const OP_TEXT = 0x1;
15
+ export const OP_BINARY = 0x2;
16
+ export const OP_CLOSE = 0x8;
17
+ export const OP_PING = 0x9;
18
+ export const OP_PONG = 0xA;
19
+
20
+ export const CLOSE_NORMAL = 1000;
21
+ export const CLOSE_GOING_AWAY = 1001;
22
+ export const CLOSE_PROTOCOL_ERROR = 1002;
23
+ export const CLOSE_UNSUPPORTED = 1003;
24
+ export const CLOSE_INVALID_PAYLOAD = 1007;
25
+ export const CLOSE_POLICY_VIOLATION = 1008;
26
+ export const CLOSE_MESSAGE_TOO_BIG = 1009;
27
+ export const CLOSE_INTERNAL_ERROR = 1011;
28
+
29
+ export class WebSocketError extends Error {
30
+ public code: number;
31
+ constructor(message: string, code = CLOSE_INTERNAL_ERROR) {
32
+ super(message);
33
+ this.name = 'WebSocketError';
34
+ this.code = code;
35
+ }
36
+ }
37
+
38
+ /** Compute Sec-WebSocket-Accept from client key per RFC 6455 Section 4.2.2 */
39
+ export function computeAcceptKey(key: string): string {
40
+ return createHash('sha1').update(key + WS_MAGIC).digest('base64');
41
+ }
42
+
43
+ export function generateWSKey(): string {
44
+ return randomBytes(16).toString('base64');
45
+ }
46
+
47
+ export function buildUpgradeResponse(key: string, headers?: Record<string, string>): string {
48
+ const accept = computeAcceptKey(key);
49
+ const lines = [
50
+ 'HTTP/1.1 101 Switching Protocols',
51
+ 'Upgrade: websocket',
52
+ 'Connection: Upgrade',
53
+ `Sec-WebSocket-Accept: ${accept}`,
54
+ ];
55
+ if (headers) {
56
+ for (const [k, v] of Object.entries(headers)) {
57
+ lines.push(`${k}: ${v}`);
58
+ }
59
+ }
60
+ lines.push('', '');
61
+ return lines.join('\r\n');
62
+ }
63
+
64
+ /** Validate Sec-WebSocket-Key: must be 16 bytes base64 encoded */
65
+ export function validateWSKey(key: string): boolean {
66
+ if (typeof key !== 'string') return false;
67
+ const decoded = Buffer.from(key, 'base64');
68
+ return decoded.length === 16;
69
+ }
70
+
71
+ export interface WSFrame {
72
+ fin: boolean;
73
+ opcode: number;
74
+ payload: Buffer;
75
+ masked: boolean;
76
+ }
77
+
78
+ /** Parse a single WebSocket frame from buffer. Returns null if incomplete. */
79
+ export function parseWSFrame(buf: Buffer, maxFrameSize = DEFAULT_MAX_WS_FRAME_SIZE): {
80
+ frame: WSFrame;
81
+ consumed: number;
82
+ } | null {
83
+ if (buf.length < 2) return null;
84
+
85
+ const firstByte = buf[0];
86
+ const secondByte = buf[1];
87
+
88
+ const fin = (firstByte & 0x80) !== 0;
89
+ const rsv1 = (firstByte & 0x40) !== 0;
90
+ const rsv2 = (firstByte & 0x20) !== 0;
91
+ const rsv3 = (firstByte & 0x10) !== 0;
92
+ const opcode = firstByte & 0x0F;
93
+ const masked = (secondByte & 0x80) !== 0;
94
+ let payloadLen = secondByte & 0x7F;
95
+
96
+ if (rsv1 || rsv2 || rsv3) {
97
+ throw new WebSocketError('RSV bits set without extension negotiation', CLOSE_PROTOCOL_ERROR);
98
+ }
99
+
100
+ let offset = 2;
101
+
102
+ if (payloadLen === 126) {
103
+ if (buf.length < 4) return null;
104
+ payloadLen = buf.readUInt16BE(2);
105
+ offset = 4;
106
+ } else if (payloadLen === 127) {
107
+ if (buf.length < 10) return null;
108
+ const high = buf.readUInt32BE(2);
109
+ const low = buf.readUInt32BE(6);
110
+ payloadLen = high * 0x100000000 + low;
111
+ if (payloadLen > Number.MAX_SAFE_INTEGER) {
112
+ throw new WebSocketError('Frame payload too large for JavaScript', CLOSE_MESSAGE_TOO_BIG);
113
+ }
114
+ offset = 10;
115
+ }
116
+
117
+ if (payloadLen > maxFrameSize) {
118
+ throw new WebSocketError(
119
+ `Frame payload exceeds max size (${maxFrameSize} bytes)`,
120
+ CLOSE_MESSAGE_TOO_BIG
121
+ );
122
+ }
123
+
124
+ let maskKey: Buffer | undefined;
125
+ if (masked) {
126
+ if (buf.length < offset + 4) return null;
127
+ maskKey = buf.subarray(offset, offset + 4);
128
+ offset += 4;
129
+ }
130
+
131
+ if (buf.length < offset + payloadLen) return null;
132
+
133
+ let payload = Buffer.from(buf.subarray(offset, offset + payloadLen));
134
+ if (masked && maskKey) {
135
+ for (let i = 0; i < payloadLen; i++) {
136
+ payload[i] ^= maskKey[i & 3];
137
+ }
138
+ }
139
+
140
+ return {
141
+ frame: { fin, opcode, payload, masked },
142
+ consumed: offset + payloadLen,
143
+ };
144
+ }
145
+
146
+ /** Create an unmasked WS frame (server-to-client per RFC 6455 Section 5.3) */
147
+ export function createWSFrame(opcode: number, payload: Buffer | string): Buffer {
148
+ const data = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload;
149
+
150
+ let header: Buffer;
151
+
152
+ if (data.length < 126) {
153
+ header = Buffer.alloc(2);
154
+ header[0] = 0x80 | opcode;
155
+ header[1] = data.length;
156
+ } else if (data.length < 65536) {
157
+ header = Buffer.alloc(4);
158
+ header[0] = 0x80 | opcode;
159
+ header[1] = 126;
160
+ header.writeUInt16BE(data.length, 2);
161
+ } else {
162
+ header = Buffer.alloc(10);
163
+ header[0] = 0x80 | opcode;
164
+ header[1] = 127;
165
+ header.writeUInt32BE(Math.floor(data.length / 0x100000000), 2);
166
+ header.writeUInt32BE(data.length & 0xFFFFFFFF, 6);
167
+ }
168
+
169
+ return Buffer.concat([header, data]);
170
+ }
171
+
172
+ /** Create a masked WS frame (client-to-server per RFC 6455 Section 5.3) */
173
+ export function createWSFrameMasked(opcode: number, payload: Buffer | string): Buffer {
174
+ const data = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload;
175
+ const maskKey = randomBytes(4);
176
+
177
+ let header: Buffer;
178
+
179
+ if (data.length < 126) {
180
+ header = Buffer.alloc(6);
181
+ header[0] = 0x80 | opcode;
182
+ header[1] = 0x80 | data.length;
183
+ maskKey.copy(header, 2);
184
+ } else if (data.length < 65536) {
185
+ header = Buffer.alloc(8);
186
+ header[0] = 0x80 | opcode;
187
+ header[1] = 0x80 | 126;
188
+ header.writeUInt16BE(data.length, 2);
189
+ maskKey.copy(header, 4);
190
+ } else {
191
+ header = Buffer.alloc(14);
192
+ header[0] = 0x80 | opcode;
193
+ header[1] = 0x80 | 127;
194
+ header.writeUInt32BE(Math.floor(data.length / 0x100000000), 2);
195
+ header.writeUInt32BE(data.length & 0xFFFFFFFF, 6);
196
+ maskKey.copy(header, 10);
197
+ }
198
+
199
+ const outData = Buffer.from(data);
200
+ for (let i = 0; i < outData.length; i++) {
201
+ outData[i] ^= maskKey[i & 3];
202
+ }
203
+
204
+ return Buffer.concat([header, outData]);
205
+ }
206
+
207
+ /* Server-to-client frame helpers (unmasked) */
208
+
209
+ export function createWSTextFrame(message: string): Buffer {
210
+ return createWSFrame(OP_TEXT, message);
211
+ }
212
+
213
+ export function createWSBinaryFrame(data: Buffer): Buffer {
214
+ return createWSFrame(OP_BINARY, data);
215
+ }
216
+
217
+ export function createWSCloseFrame(code = CLOSE_NORMAL, reason = ''): Buffer {
218
+ const buf = Buffer.alloc(2 + Buffer.byteLength(reason));
219
+ buf.writeUInt16BE(code, 0);
220
+ if (reason.length > 0) buf.write(reason, 2, 'utf8');
221
+ return createWSFrame(OP_CLOSE, buf);
222
+ }
223
+
224
+ export function createWSPingFrame(data?: Buffer): Buffer {
225
+ return createWSFrame(OP_PING, data || Buffer.alloc(0));
226
+ }
227
+
228
+ export function createWSPongFrame(data?: Buffer): Buffer {
229
+ return createWSFrame(OP_PONG, data || Buffer.alloc(0));
230
+ }
231
+
232
+ /* Client-to-server frame helpers (masked) */
233
+
234
+ export function createWSTextFrameMasked(message: string): Buffer {
235
+ return createWSFrameMasked(OP_TEXT, message);
236
+ }
237
+
238
+ export function createWSBinaryFrameMasked(data: Buffer): Buffer {
239
+ return createWSFrameMasked(OP_BINARY, data);
240
+ }
241
+
242
+ export function createWSCloseFrameMasked(code = CLOSE_NORMAL, reason = ''): Buffer {
243
+ const buf = Buffer.alloc(2 + Buffer.byteLength(reason));
244
+ buf.writeUInt16BE(code, 0);
245
+ if (reason.length > 0) buf.write(reason, 2, 'utf8');
246
+ return createWSFrameMasked(OP_CLOSE, buf);
247
+ }
248
+
249
+ export function createWSPingFrameMasked(): Buffer {
250
+ return createWSFrameMasked(OP_PING, Buffer.alloc(0));
251
+ }
252
+
253
+ export function createWSPongFrameMasked(): Buffer {
254
+ return createWSFrameMasked(OP_PONG, Buffer.alloc(0));
255
+ }
256
+
257
+ /** Streaming parser for WebSocket frames. Buffers partial data and emits complete frames. */
258
+ export class WSFrameParser {
259
+ private buf: Buffer = Buffer.alloc(0);
260
+ private maxFrameSize: number;
261
+ private totalBytesReceived = 0;
262
+
263
+ constructor(maxFrameSize = DEFAULT_MAX_WS_FRAME_SIZE) {
264
+ this.maxFrameSize = maxFrameSize;
265
+ }
266
+
267
+ feed(data: Buffer): WSFrame[] {
268
+ this.totalBytesReceived += data.length;
269
+ this.buf = Buffer.concat([this.buf, data]);
270
+
271
+ if (this.buf.length > this.maxFrameSize * 2) {
272
+ this.buf = Buffer.alloc(0);
273
+ throw new WebSocketError(
274
+ `Input buffer exceeded limit`,
275
+ CLOSE_POLICY_VIOLATION
276
+ );
277
+ }
278
+
279
+ const frames: WSFrame[] = [];
280
+
281
+ while (this.buf.length > 0) {
282
+ try {
283
+ const result = parseWSFrame(this.buf, this.maxFrameSize);
284
+ if (!result) break;
285
+
286
+ const { frame, consumed } = result;
287
+
288
+ if (frame.opcode === OP_CONTINUATION) {
289
+ this.buf = consumed < this.buf.length
290
+ ? Buffer.from(this.buf.subarray(consumed))
291
+ : Buffer.alloc(0);
292
+ continue;
293
+ }
294
+
295
+ frames.push(frame);
296
+ this.buf = consumed < this.buf.length
297
+ ? Buffer.from(this.buf.subarray(consumed))
298
+ : Buffer.alloc(0);
299
+ } catch (err) {
300
+ this.buf = Buffer.alloc(0);
301
+ throw err;
302
+ }
303
+ }
304
+
305
+ return frames;
306
+ }
307
+
308
+ reset(): void {
309
+ this.buf = Buffer.alloc(0);
310
+ this.totalBytesReceived = 0;
311
+ }
312
+
313
+ getBytesReceived(): number {
314
+ return this.totalBytesReceived;
315
+ }
316
+ }