stelar-time-real 3.3.0 → 3.3.2

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/src/websocket.ts CHANGED
@@ -1,10 +1,9 @@
1
- /**
2
- * @stelar-time-real WebSocket (RFC 6455)
3
- */
1
+ /** @stelar-time-real WebSocket (RFC 6455) + permessage-deflate (RFC 7692) */
4
2
 
5
3
  import { createHash, randomBytes } from 'crypto';
4
+ import { deflateRawSync, inflateRawSync } from 'zlib';
6
5
 
7
- const WS_MAGIC = '258EAFA5-E914-47DA-95CA-5AB5A7E3A741';
6
+ const WS_MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
8
7
  export const DEFAULT_MAX_WS_FRAME_SIZE = 10 * 1024 * 1024;
9
8
 
10
9
  export const OP_CONTINUATION = 0x0, OP_TEXT = 0x1, OP_BINARY = 0x2,
@@ -23,21 +22,39 @@ export const computeAcceptKey = (key: string) => createHash('sha1').update(key +
23
22
  export const generateWSKey = () => randomBytes(16).toString('base64');
24
23
  export const validateWSKey = (key: string): boolean => typeof key === 'string' && Buffer.from(key, 'base64').length === 16;
25
24
 
26
- export function buildUpgradeResponse(key: string, headers?: Record<string, string>): string {
25
+ export function buildUpgradeResponse(key: string, headers?: Record<string, string>, compress = false): string {
27
26
  const lines = ['HTTP/1.1 101 Switching Protocols', 'Upgrade: websocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${computeAcceptKey(key)}`];
27
+ if (compress) lines.push('Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_no_context_takeover');
28
28
  if (headers) for (const [k, v] of Object.entries(headers)) lines.push(`${k}: ${v}`);
29
29
  lines.push('', '');
30
30
  return lines.join('\r\n');
31
31
  }
32
32
 
33
- export interface WSFrame { fin: boolean; opcode: number; payload: Buffer; masked: boolean; }
33
+ /** Parse client's Sec-WebSocket-Extensions header to check for permessage-deflate */
34
+ export function clientWantsCompression(extensionsHeader: string | undefined): boolean {
35
+ if (!extensionsHeader) return false;
36
+ return /permessage-deflate/.test(extensionsHeader);
37
+ }
38
+
39
+ export interface WSFrame { fin: boolean; opcode: number; payload: Buffer; masked: boolean; rsv1: boolean; }
40
+
41
+ const TAIL = Buffer.from([0x00, 0x00, 0xFF, 0xFF]);
42
+
43
+ export function wsCompress(data: Buffer): Buffer {
44
+ const deflated = deflateRawSync(data);
45
+ return deflated.subarray(0, deflated.length - 4);
46
+ }
47
+
48
+ export function wsDecompress(data: Buffer): Buffer {
49
+ return inflateRawSync(Buffer.concat([data, TAIL]));
50
+ }
34
51
 
35
52
  export function parseWSFrame(buf: Buffer, max = DEFAULT_MAX_WS_FRAME_SIZE): { frame: WSFrame; consumed: number } | null {
36
53
  if (buf.length < 2) return null;
37
54
  const b0 = buf[0], b1 = buf[1];
38
- const fin = !!(b0 & 0x80), rsv = b0 & 0x70, opcode = b0 & 0x0F, masked = !!(b1 & 0x80);
55
+ const fin = !!(b0 & 0x80), rsv1 = !!(b0 & 0x40), rsv = b0 & 0x30, opcode = b0 & 0x0F, masked = !!(b1 & 0x80);
39
56
  let len = b1 & 0x7F, off = 2;
40
- if (rsv) throw new WebSocketError('RSV bits set', CLOSE_PROTOCOL_ERROR);
57
+ if (rsv) throw new WebSocketError('RSV2/RSV3 bits set', CLOSE_PROTOCOL_ERROR);
41
58
  if (len === 126) { if (buf.length < 4) return null; len = buf.readUInt16BE(2); off = 4; }
42
59
  else if (len === 127) { if (buf.length < 10) return null; len = buf.readUInt32BE(2) * 0x100000000 + buf.readUInt32BE(6); off = 10; }
43
60
  if (len > max) throw new WebSocketError(`Frame exceeds max (${max})`, CLOSE_MESSAGE_TOO_BIG);
@@ -46,31 +63,34 @@ export function parseWSFrame(buf: Buffer, max = DEFAULT_MAX_WS_FRAME_SIZE): { fr
46
63
  if (buf.length < off + len) return null;
47
64
  const payload = Buffer.from(buf.subarray(off, off + len));
48
65
  if (masked && mk) for (let i = 0; i < len; i++) payload[i] ^= mk[i & 3];
49
- return { frame: { fin, opcode, payload, masked }, consumed: off + len };
66
+ return { frame: { fin, opcode, payload, masked, rsv1 }, consumed: off + len };
50
67
  }
51
68
 
52
- export function createWSFrame(opcode: number, payload: Buffer | string, masked = false): Buffer {
69
+ export function createWSFrame(opcode: number, payload: Buffer | string, masked = false, compress = false): Buffer {
53
70
  const d = typeof payload === 'string' ? Buffer.from(payload, 'utf8') : payload;
71
+ const compressed = compress && (opcode === OP_TEXT || opcode === OP_BINARY) ? wsCompress(d) : d;
72
+ const actualOpcode = compressed !== d ? opcode : opcode;
54
73
  const mk = masked ? randomBytes(4) : undefined;
55
74
  const base = masked ? 0x80 : 0;
75
+ const rsv1Bit = (compress && (opcode === OP_TEXT || opcode === OP_BINARY) && compressed !== d) ? 0x40 : 0;
56
76
  let h: Buffer;
57
- if (d.length < 126) {
58
- h = Buffer.alloc(masked ? 6 : 2); h[0] = 0x80 | opcode; h[1] = base | d.length;
77
+ if (compressed.length < 126) {
78
+ h = Buffer.alloc(masked ? 6 : 2); h[0] = 0x80 | rsv1Bit | actualOpcode; h[1] = base | compressed.length;
59
79
  if (mk) mk.copy(h, 2);
60
- } else if (d.length < 65536) {
61
- h = Buffer.alloc(masked ? 8 : 4); h[0] = 0x80 | opcode; h[1] = base | 126;
62
- h.writeUInt16BE(d.length, 2); if (mk) mk.copy(h, 4);
80
+ } else if (compressed.length < 65536) {
81
+ h = Buffer.alloc(masked ? 8 : 4); h[0] = 0x80 | rsv1Bit | actualOpcode; h[1] = base | 126;
82
+ h.writeUInt16BE(compressed.length, 2); if (mk) mk.copy(h, 4);
63
83
  } else {
64
- h = Buffer.alloc(masked ? 14 : 10); h[0] = 0x80 | opcode; h[1] = base | 127;
65
- h.writeUInt32BE(Math.floor(d.length / 0x100000000), 2);
66
- h.writeUInt32BE(d.length & 0xFFFFFFFF, 6); if (mk) mk.copy(h, 10);
84
+ h = Buffer.alloc(masked ? 14 : 10); h[0] = 0x80 | rsv1Bit | actualOpcode; h[1] = base | 127;
85
+ h.writeUInt32BE(Math.floor(compressed.length / 0x100000000), 2);
86
+ h.writeUInt32BE(compressed.length & 0xFFFFFFFF, 6); if (mk) mk.copy(h, 10);
67
87
  }
68
- if (mk) for (let i = 0; i < d.length; i++) d[i] ^= mk[i & 3];
69
- return Buffer.concat([h, d]);
88
+ if (mk) for (let i = 0; i < compressed.length; i++) compressed[i] ^= mk[i & 3];
89
+ return Buffer.concat([h, compressed]);
70
90
  }
71
91
 
72
92
  /* Server (unmasked) */
73
- export const createWSTextFrame = (msg: string) => createWSFrame(OP_TEXT, msg);
93
+ export const createWSTextFrame = (msg: string, compress = false) => createWSFrame(OP_TEXT, msg, false, compress);
74
94
  export const createWSBinaryFrame = (data: Buffer) => createWSFrame(OP_BINARY, data);
75
95
  export const createWSCloseFrame = (code = CLOSE_NORMAL, reason = '') => {
76
96
  const b = Buffer.alloc(2 + Buffer.byteLength(reason)); b.writeUInt16BE(code, 0); if (reason) b.write(reason, 2, 'utf8'); return createWSFrame(OP_CLOSE, b);
@@ -79,7 +99,7 @@ export const createWSPingFrame = (data?: Buffer) => createWSFrame(OP_PING, data
79
99
  export const createWSPongFrame = (data?: Buffer) => createWSFrame(OP_PONG, data || Buffer.alloc(0));
80
100
 
81
101
  /* Client (masked) */
82
- export const createWSTextFrameMasked = (msg: string) => createWSFrame(OP_TEXT, msg, true);
102
+ export const createWSTextFrameMasked = (msg: string, compress = false) => createWSFrame(OP_TEXT, msg, true, compress);
83
103
  export const createWSBinaryFrameMasked = (data: Buffer) => createWSFrame(OP_BINARY, data, true);
84
104
  export const createWSCloseFrameMasked = (code = CLOSE_NORMAL, reason = '') => {
85
105
  const b = Buffer.alloc(2 + Buffer.byteLength(reason)); b.writeUInt16BE(code, 0); if (reason) b.write(reason, 2, 'utf8'); return createWSFrame(OP_CLOSE, b, true);
@@ -87,29 +107,47 @@ export const createWSCloseFrameMasked = (code = CLOSE_NORMAL, reason = '') => {
87
107
  export const createWSPingFrameMasked = () => createWSFrame(OP_PING, Buffer.alloc(0), true);
88
108
  export const createWSPongFrameMasked = () => createWSFrame(OP_PONG, Buffer.alloc(0), true);
89
109
 
110
+ /** Streaming WS frame parser — O(1) append, avoids Buffer.concat O(n²) */
90
111
  export class WSFrameParser {
91
- private buf = Buffer.alloc(0);
112
+ private chunks: Buffer[] = [];
113
+ private len = 0;
92
114
  private max: number;
93
115
  private received = 0;
94
116
 
95
117
  constructor(max = DEFAULT_MAX_WS_FRAME_SIZE) { this.max = max; }
96
118
 
119
+ private _compact(): Buffer {
120
+ if (this.chunks.length <= 1) return this.chunks[0] || Buffer.alloc(0);
121
+ const buf = Buffer.concat(this.chunks);
122
+ this.chunks = [buf];
123
+ return buf;
124
+ }
125
+
97
126
  feed(data: Buffer): WSFrame[] {
98
127
  this.received += data.length;
99
- this.buf = Buffer.concat([this.buf, data]);
100
- if (this.buf.length > this.max * 2) { this.buf = Buffer.alloc(0); throw new WebSocketError('Buffer overflow', CLOSE_POLICY_VIOLATION); }
128
+ this.chunks.push(data);
129
+ this.len += data.length;
130
+ if (this.len > this.max * 2) { this.chunks = []; this.len = 0; throw new WebSocketError('Buffer overflow', CLOSE_POLICY_VIOLATION); }
101
131
  const frames: WSFrame[] = [];
102
- while (this.buf.length > 0) {
132
+ while (this.len > 0) {
133
+ const buf = this._compact();
103
134
  try {
104
- const r = parseWSFrame(this.buf, this.max);
135
+ const r = parseWSFrame(buf, this.max);
105
136
  if (!r) break;
106
- this.buf = r.consumed < this.buf.length ? Buffer.from(this.buf.subarray(r.consumed)) : Buffer.alloc(0);
107
- if (r.frame.opcode !== OP_CONTINUATION) frames.push(r.frame);
108
- } catch (e) { this.buf = Buffer.alloc(0); throw e; }
137
+ const consumed = r.consumed;
138
+ if (consumed < buf.length) { this.chunks = [Buffer.from(buf.subarray(consumed))]; this.len = this.chunks[0].length; }
139
+ else { this.chunks = []; this.len = 0; }
140
+ if (r.frame.opcode !== OP_CONTINUATION) {
141
+ if (r.frame.rsv1 && (r.frame.opcode === OP_TEXT || r.frame.opcode === OP_BINARY)) {
142
+ try { r.frame.payload = wsDecompress(r.frame.payload); r.frame.rsv1 = false; } catch { throw new WebSocketError('Decompression failed', CLOSE_INVALID_PAYLOAD); }
143
+ }
144
+ frames.push(r.frame);
145
+ }
146
+ } catch (e) { this.chunks = []; this.len = 0; throw e; }
109
147
  }
110
148
  return frames;
111
149
  }
112
150
 
113
- reset() { this.buf = Buffer.alloc(0); this.received = 0; }
151
+ reset() { this.chunks = []; this.len = 0; this.received = 0; }
114
152
  getBytesReceived() { return this.received; }
115
153
  }