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/package.json +1 -1
- package/src/client.d.ts +10 -3
- package/src/client.d.ts.map +1 -1
- package/src/client.js +105 -58
- package/src/client.ts +92 -42
- package/src/index.d.ts +8 -6
- package/src/index.d.ts.map +1 -1
- package/src/index.js +161 -105
- package/src/index.ts +127 -79
- package/src/logger.d.ts +0 -1
- package/src/logger.d.ts.map +1 -1
- package/src/logger.js +5 -8
- package/src/logger.ts +6 -10
- package/src/protocol.d.ts +5 -5
- package/src/protocol.d.ts.map +1 -1
- package/src/protocol.js +39 -25
- package/src/protocol.ts +31 -41
- package/src/websocket.d.ts +14 -8
- package/src/websocket.d.ts.map +1 -1
- package/src/websocket.js +82 -33
- package/src/websocket.ts +69 -31
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-
|
|
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
|
|
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
|
-
|
|
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 &
|
|
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('
|
|
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 (
|
|
58
|
-
h = Buffer.alloc(masked ? 6 : 2); h[0] = 0x80 |
|
|
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 (
|
|
61
|
-
h = Buffer.alloc(masked ? 8 : 4); h[0] = 0x80 |
|
|
62
|
-
h.writeUInt16BE(
|
|
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 |
|
|
65
|
-
h.writeUInt32BE(Math.floor(
|
|
66
|
-
h.writeUInt32BE(
|
|
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 <
|
|
69
|
-
return Buffer.concat([h,
|
|
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
|
|
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.
|
|
100
|
-
|
|
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.
|
|
132
|
+
while (this.len > 0) {
|
|
133
|
+
const buf = this._compact();
|
|
103
134
|
try {
|
|
104
|
-
const r = parseWSFrame(
|
|
135
|
+
const r = parseWSFrame(buf, this.max);
|
|
105
136
|
if (!r) break;
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
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.
|
|
151
|
+
reset() { this.chunks = []; this.len = 0; this.received = 0; }
|
|
114
152
|
getBytesReceived() { return this.received; }
|
|
115
153
|
}
|