@livedesk/hub 0.1.41 → 0.1.43
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 +3 -3
- package/src/agents/agent-device-scope.js +29 -29
- package/src/agents/agent-manager.js +103 -103
- package/src/agents/agent-permission-store.js +78 -78
- package/src/agents/agent-runtime-error.js +9 -9
- package/src/agents/agent-settings.js +71 -71
- package/src/agents/codex-agent-runtime.js +594 -594
- package/src/agents/codex-mcp-server.js +100 -100
- package/src/console-relay.js +415 -0
- package/src/filesystem/directory-reader.js +187 -187
- package/src/filesystem/hub-filesystem.js +78 -78
- package/src/filesystem/path-registry.js +78 -78
- package/src/filesystem/roots.js +115 -115
- package/src/frame-packet-contract.mjs +427 -427
- package/src/http/hub-ui-session.js +119 -119
- package/src/live-capture-transition-retry.mjs +297 -297
- package/src/live-desk-update.js +167 -108
- package/src/live-stream-monitor-contract.js +38 -38
- package/src/lzo1x.js +109 -109
- package/src/mode4-atlas-pool.js +137 -137
- package/src/mode4-atlas-sizing.js +32 -32
- package/src/mode4-atlas-worker.js +599 -599
- package/src/mode4-atlas.js +306 -306
- package/src/remote-audio-subscription-contract.mjs +109 -109
- package/src/remote-audio-subscription-contract.test.mjs +72 -72
- package/src/remote-hub.js +144 -144
- package/src/server.js +244 -222
- package/src/settings/effective-device-policy.js +16 -16
- package/src/transport/udp-protocol.js +453 -453
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import crypto from 'node:crypto';
|
|
2
|
-
import { inflateRawSync } from 'node:zlib';
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import { inflateRawSync } from 'node:zlib';
|
|
3
3
|
|
|
4
4
|
export const UDP_P2P_PROTOCOL = 'livedesk.udp.p2p.v1';
|
|
5
|
-
export const UDP_P2P_VERSION = 1;
|
|
6
|
-
export const UDP_MAX_DATAGRAM_BYTES = 1200;
|
|
7
|
-
const UDP_SERIAL_MODULUS = 0x1_0000_0000;
|
|
8
|
-
const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
|
|
9
|
-
export const UDP_PACKET_TYPES = Object.freeze({
|
|
5
|
+
export const UDP_P2P_VERSION = 1;
|
|
6
|
+
export const UDP_MAX_DATAGRAM_BYTES = 1200;
|
|
7
|
+
const UDP_SERIAL_MODULUS = 0x1_0000_0000;
|
|
8
|
+
const UDP_SERIAL_HALF_RANGE = 0x8000_0000;
|
|
9
|
+
export const UDP_PACKET_TYPES = Object.freeze({
|
|
10
10
|
probe: 1,
|
|
11
11
|
'probe-ack': 2,
|
|
12
12
|
ready: 3,
|
|
@@ -14,20 +14,20 @@ export const UDP_PACKET_TYPES = Object.freeze({
|
|
|
14
14
|
pong: 5,
|
|
15
15
|
frame: 6,
|
|
16
16
|
close: 7
|
|
17
|
-
});
|
|
18
|
-
const UDP_PACKET_TYPE_NAMES = new Map(Object.entries(UDP_PACKET_TYPES).map(([name, value]) => [value, name]));
|
|
19
|
-
const UDP_FIXED_HEADER_BYTES = 15;
|
|
20
|
-
const UDP_NONCE_BYTES = 12;
|
|
21
|
-
const UDP_TAG_BYTES = 16;
|
|
22
|
-
const UDP_MAX_SESSION_ID_BYTES = 96;
|
|
23
|
-
const UDP_MAX_HEADER_BYTES = 4096;
|
|
24
|
-
const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
|
|
25
|
-
const UDP_FRAME_PAYLOAD_MAGIC = Buffer.from('LDF1', 'ascii');
|
|
26
|
-
const UDP_PACKET_MAGIC = Buffer.from('LDU1', 'ascii');
|
|
27
|
-
const UDP_FRAME_CHUNK_MAGIC = Buffer.from('LDC1', 'ascii');
|
|
28
|
-
export const UDP_FRAME_CHUNK_PROTOCOL = 'livedesk.udp.frame-chunk.v1';
|
|
29
|
-
export const UDP_FRAME_CHUNK_HEADER_BYTES = 12;
|
|
30
|
-
const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
|
|
17
|
+
});
|
|
18
|
+
const UDP_PACKET_TYPE_NAMES = new Map(Object.entries(UDP_PACKET_TYPES).map(([name, value]) => [value, name]));
|
|
19
|
+
const UDP_FIXED_HEADER_BYTES = 15;
|
|
20
|
+
const UDP_NONCE_BYTES = 12;
|
|
21
|
+
const UDP_TAG_BYTES = 16;
|
|
22
|
+
const UDP_MAX_SESSION_ID_BYTES = 96;
|
|
23
|
+
const UDP_MAX_HEADER_BYTES = 4096;
|
|
24
|
+
const UDP_MAX_PAYLOAD_BYTES = UDP_MAX_DATAGRAM_BYTES - UDP_FIXED_HEADER_BYTES - UDP_NONCE_BYTES - UDP_TAG_BYTES - 1;
|
|
25
|
+
const UDP_FRAME_PAYLOAD_MAGIC = Buffer.from('LDF1', 'ascii');
|
|
26
|
+
const UDP_PACKET_MAGIC = Buffer.from('LDU1', 'ascii');
|
|
27
|
+
const UDP_FRAME_CHUNK_MAGIC = Buffer.from('LDC1', 'ascii');
|
|
28
|
+
export const UDP_FRAME_CHUNK_PROTOCOL = 'livedesk.udp.frame-chunk.v1';
|
|
29
|
+
export const UDP_FRAME_CHUNK_HEADER_BYTES = 12;
|
|
30
|
+
const UDP_MAX_FRAME_HEADER_BYTES = 64 * 1024;
|
|
31
31
|
|
|
32
32
|
function asBuffer(value) {
|
|
33
33
|
if (Buffer.isBuffer(value)) return value;
|
|
@@ -41,244 +41,244 @@ function normalizeKey(key) {
|
|
|
41
41
|
return value.length === 32 ? value : crypto.createHash('sha256').update(value).digest();
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
function normalizeSessionId(value) {
|
|
44
|
+
function normalizeSessionId(value) {
|
|
45
45
|
const sessionId = String(value || '').trim();
|
|
46
46
|
if (!sessionId || Buffer.byteLength(sessionId, 'utf8') > UDP_MAX_SESSION_ID_BYTES) {
|
|
47
47
|
throw new Error('invalid-udp-session-id');
|
|
48
48
|
}
|
|
49
49
|
return sessionId;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function isUdpBinaryDatagram(packet) {
|
|
53
|
-
return Buffer.isBuffer(packet)
|
|
54
|
-
&& packet.length >= UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + UDP_TAG_BYTES
|
|
55
|
-
&& packet[0] === UDP_PACKET_MAGIC[0]
|
|
56
|
-
&& packet[1] === UDP_PACKET_MAGIC[1]
|
|
57
|
-
&& packet[2] === UDP_PACKET_MAGIC[2]
|
|
58
|
-
&& packet[3] === UDP_PACKET_MAGIC[3]
|
|
59
|
-
&& packet[4] === UDP_P2P_VERSION;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function peekUdpSessionId(packet) {
|
|
63
|
-
if (!isUdpBinaryDatagram(packet)) return '';
|
|
64
|
-
const sessionLength = packet[6];
|
|
65
|
-
const headerLength = packet.readUInt16BE(7);
|
|
66
|
-
const payloadLength = packet.readUInt16BE(9);
|
|
67
|
-
const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
|
|
68
|
-
const expectedLength = associatedLength + payloadLength + UDP_TAG_BYTES;
|
|
69
|
-
if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
|
|
70
|
-
|| headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
|
|
71
|
-
|| expectedLength !== packet.length) return '';
|
|
72
|
-
return packet.subarray(
|
|
73
|
-
UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
|
|
74
|
-
UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength
|
|
75
|
-
).toString('utf8');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function routeKeyForBytes(bytes, offset = 0, length = bytes.length - offset) {
|
|
79
|
-
let hash = 0x811c9dc5;
|
|
80
|
-
const end = offset + length;
|
|
81
|
-
for (let index = offset; index < end; index += 1) {
|
|
82
|
-
hash = Math.imul(hash ^ bytes[index], 0x01000193) >>> 0;
|
|
83
|
-
}
|
|
84
|
-
return hash;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function udpSessionRouteKey(sessionId) {
|
|
88
|
-
return routeKeyForBytes(Buffer.from(normalizeSessionId(sessionId), 'utf8'));
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function peekUdpSessionRouteKey(packet) {
|
|
92
|
-
if (!isUdpBinaryDatagram(packet)) return null;
|
|
93
|
-
const sessionLength = packet[6];
|
|
94
|
-
const headerLength = packet.readUInt16BE(7);
|
|
95
|
-
const payloadLength = packet.readUInt16BE(9);
|
|
96
|
-
const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
|
|
97
|
-
if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
|
|
98
|
-
|| headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
|
|
99
|
-
|| associatedLength + payloadLength + UDP_TAG_BYTES !== packet.length) return null;
|
|
100
|
-
return routeKeyForBytes(
|
|
101
|
-
packet,
|
|
102
|
-
UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
|
|
103
|
-
sessionLength
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function encodeFrameChunkHeader(header) {
|
|
108
|
-
const frameSerial = Number(header?.frameSerial);
|
|
109
|
-
const chunkIndex = Number(header?.chunkIndex);
|
|
110
|
-
const chunkCount = Number(header?.chunkCount);
|
|
111
|
-
if (!Number.isInteger(frameSerial) || frameSerial < 0 || frameSerial > 0xffff_ffff
|
|
112
|
-
|| !Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex > 0xffff
|
|
113
|
-
|| !Number.isInteger(chunkCount) || chunkCount < 1 || chunkCount > 4096
|
|
114
|
-
|| chunkIndex >= chunkCount) {
|
|
115
|
-
throw new Error('invalid-udp-frame-chunk-header');
|
|
116
|
-
}
|
|
117
|
-
const bytes = Buffer.allocUnsafe(UDP_FRAME_CHUNK_HEADER_BYTES);
|
|
118
|
-
UDP_FRAME_CHUNK_MAGIC.copy(bytes, 0);
|
|
119
|
-
bytes.writeUInt32BE(frameSerial >>> 0, 4);
|
|
120
|
-
bytes.writeUInt16BE(chunkIndex, 8);
|
|
121
|
-
bytes.writeUInt16BE(chunkCount, 10);
|
|
122
|
-
return bytes;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function decodePacketHeader(type, bytes) {
|
|
126
|
-
if (type === 'frame'
|
|
127
|
-
&& bytes.length === UDP_FRAME_CHUNK_HEADER_BYTES
|
|
128
|
-
&& bytes[0] === UDP_FRAME_CHUNK_MAGIC[0]
|
|
129
|
-
&& bytes[1] === UDP_FRAME_CHUNK_MAGIC[1]
|
|
130
|
-
&& bytes[2] === UDP_FRAME_CHUNK_MAGIC[2]
|
|
131
|
-
&& bytes[3] === UDP_FRAME_CHUNK_MAGIC[3]) {
|
|
132
|
-
const chunkIndex = bytes.readUInt16BE(8);
|
|
133
|
-
const chunkCount = bytes.readUInt16BE(10);
|
|
134
|
-
if (chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
|
|
135
|
-
const frameSerial = bytes.readUInt32BE(4);
|
|
136
|
-
return {
|
|
137
|
-
frameId: frameSerial,
|
|
138
|
-
frameSerial,
|
|
139
|
-
chunkIndex,
|
|
140
|
-
chunkCount,
|
|
141
|
-
binaryChunkHeader: true
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
try {
|
|
145
|
-
const value = JSON.parse(bytes.toString('utf8'));
|
|
146
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
147
|
-
} catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function decodeFrameHeader(header) {
|
|
153
|
-
if (header?.frameHeader && typeof header.frameHeader === 'object') {
|
|
154
|
-
return header.frameHeader;
|
|
155
|
-
}
|
|
156
|
-
const encoded = String(header?.frameHeaderDeflate || '');
|
|
157
|
-
if (!encoded || encoded.length > 8192 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
|
|
158
|
-
return null;
|
|
159
|
-
}
|
|
160
|
-
try {
|
|
161
|
-
const json = inflateRawSync(Buffer.from(encoded, 'base64'), { maxOutputLength: 64 * 1024 }).toString('utf8');
|
|
162
|
-
const value = JSON.parse(json);
|
|
163
|
-
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
164
|
-
} catch {
|
|
165
|
-
return null;
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
function decodeFramedPayload(payload, legacyHeader = null) {
|
|
170
|
-
const bytes = asBuffer(payload);
|
|
171
|
-
if (bytes.length < 8 || !bytes.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
|
|
172
|
-
return { header: legacyHeader || {}, payload: bytes };
|
|
173
|
-
}
|
|
174
|
-
const compressedHeaderLength = bytes.readUInt32BE(4);
|
|
175
|
-
if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
|
|
176
|
-
|| 8 + compressedHeaderLength > bytes.length) return null;
|
|
177
|
-
try {
|
|
178
|
-
const json = inflateRawSync(bytes.subarray(8, 8 + compressedHeaderLength), {
|
|
179
|
-
maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
|
|
180
|
-
}).toString('utf8');
|
|
181
|
-
const header = JSON.parse(json);
|
|
182
|
-
if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
|
|
183
|
-
return {
|
|
184
|
-
header,
|
|
185
|
-
payload: bytes.subarray(8 + compressedHeaderLength)
|
|
186
|
-
};
|
|
187
|
-
} catch {
|
|
188
|
-
return null;
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
function copyChunkRange(chunks, sourceOffset, length, target, targetOffset = 0) {
|
|
193
|
-
let remaining = length;
|
|
194
|
-
let skip = sourceOffset;
|
|
195
|
-
let written = 0;
|
|
196
|
-
for (const chunk of chunks) {
|
|
197
|
-
if (!chunk || remaining <= 0) continue;
|
|
198
|
-
if (skip >= chunk.length) {
|
|
199
|
-
skip -= chunk.length;
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
const available = Math.min(remaining, chunk.length - skip);
|
|
203
|
-
chunk.copy(target, targetOffset + written, skip, skip + available);
|
|
204
|
-
written += available;
|
|
205
|
-
remaining -= available;
|
|
206
|
-
skip = 0;
|
|
207
|
-
}
|
|
208
|
-
return written === length;
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function decodeFramedChunks(chunks, totalBytes, legacyHeader = null) {
|
|
212
|
-
if (chunks.length === 1) {
|
|
213
|
-
const complete = decodeFramedPayload(chunks[0], legacyHeader);
|
|
214
|
-
return complete
|
|
215
|
-
? { ...complete, payloadCopyCount: 0, payloadCopyBytes: 0 }
|
|
216
|
-
: null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
const prefix = Buffer.allocUnsafe(Math.min(8, totalBytes));
|
|
220
|
-
if (!copyChunkRange(chunks, 0, prefix.length, prefix)) return null;
|
|
221
|
-
if (prefix.length < 8 || !prefix.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
|
|
222
|
-
const payload = Buffer.allocUnsafe(totalBytes);
|
|
223
|
-
if (!copyChunkRange(chunks, 0, totalBytes, payload)) return null;
|
|
224
|
-
return {
|
|
225
|
-
header: legacyHeader || {},
|
|
226
|
-
payload,
|
|
227
|
-
payloadCopyCount: 1,
|
|
228
|
-
payloadCopyBytes: totalBytes
|
|
229
|
-
};
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
const compressedHeaderLength = prefix.readUInt32BE(4);
|
|
233
|
-
const payloadOffset = 8 + compressedHeaderLength;
|
|
234
|
-
if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
|
|
235
|
-
|| payloadOffset > totalBytes) return null;
|
|
236
|
-
try {
|
|
237
|
-
const compressedHeader = Buffer.allocUnsafe(compressedHeaderLength);
|
|
238
|
-
if (!copyChunkRange(chunks, 8, compressedHeaderLength, compressedHeader)) return null;
|
|
239
|
-
const json = inflateRawSync(compressedHeader, {
|
|
240
|
-
maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
|
|
241
|
-
}).toString('utf8');
|
|
242
|
-
const header = JSON.parse(json);
|
|
243
|
-
if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
|
|
244
|
-
const payloadLength = totalBytes - payloadOffset;
|
|
245
|
-
const payload = Buffer.allocUnsafe(payloadLength);
|
|
246
|
-
if (!copyChunkRange(chunks, payloadOffset, payloadLength, payload)) return null;
|
|
247
|
-
return {
|
|
248
|
-
header,
|
|
249
|
-
payload,
|
|
250
|
-
payloadCopyCount: 1,
|
|
251
|
-
payloadCopyBytes: payloadLength
|
|
252
|
-
};
|
|
253
|
-
} catch {
|
|
254
|
-
return null;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function packetTypeCode(type) {
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isUdpBinaryDatagram(packet) {
|
|
53
|
+
return Buffer.isBuffer(packet)
|
|
54
|
+
&& packet.length >= UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + UDP_TAG_BYTES
|
|
55
|
+
&& packet[0] === UDP_PACKET_MAGIC[0]
|
|
56
|
+
&& packet[1] === UDP_PACKET_MAGIC[1]
|
|
57
|
+
&& packet[2] === UDP_PACKET_MAGIC[2]
|
|
58
|
+
&& packet[3] === UDP_PACKET_MAGIC[3]
|
|
59
|
+
&& packet[4] === UDP_P2P_VERSION;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function peekUdpSessionId(packet) {
|
|
63
|
+
if (!isUdpBinaryDatagram(packet)) return '';
|
|
64
|
+
const sessionLength = packet[6];
|
|
65
|
+
const headerLength = packet.readUInt16BE(7);
|
|
66
|
+
const payloadLength = packet.readUInt16BE(9);
|
|
67
|
+
const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
|
|
68
|
+
const expectedLength = associatedLength + payloadLength + UDP_TAG_BYTES;
|
|
69
|
+
if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
|
|
70
|
+
|| headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
|
|
71
|
+
|| expectedLength !== packet.length) return '';
|
|
72
|
+
return packet.subarray(
|
|
73
|
+
UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
|
|
74
|
+
UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength
|
|
75
|
+
).toString('utf8');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function routeKeyForBytes(bytes, offset = 0, length = bytes.length - offset) {
|
|
79
|
+
let hash = 0x811c9dc5;
|
|
80
|
+
const end = offset + length;
|
|
81
|
+
for (let index = offset; index < end; index += 1) {
|
|
82
|
+
hash = Math.imul(hash ^ bytes[index], 0x01000193) >>> 0;
|
|
83
|
+
}
|
|
84
|
+
return hash;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function udpSessionRouteKey(sessionId) {
|
|
88
|
+
return routeKeyForBytes(Buffer.from(normalizeSessionId(sessionId), 'utf8'));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function peekUdpSessionRouteKey(packet) {
|
|
92
|
+
if (!isUdpBinaryDatagram(packet)) return null;
|
|
93
|
+
const sessionLength = packet[6];
|
|
94
|
+
const headerLength = packet.readUInt16BE(7);
|
|
95
|
+
const payloadLength = packet.readUInt16BE(9);
|
|
96
|
+
const associatedLength = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES + sessionLength + headerLength;
|
|
97
|
+
if (sessionLength < 1 || sessionLength > UDP_MAX_SESSION_ID_BYTES
|
|
98
|
+
|| headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
|
|
99
|
+
|| associatedLength + payloadLength + UDP_TAG_BYTES !== packet.length) return null;
|
|
100
|
+
return routeKeyForBytes(
|
|
101
|
+
packet,
|
|
102
|
+
UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES,
|
|
103
|
+
sessionLength
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function encodeFrameChunkHeader(header) {
|
|
108
|
+
const frameSerial = Number(header?.frameSerial);
|
|
109
|
+
const chunkIndex = Number(header?.chunkIndex);
|
|
110
|
+
const chunkCount = Number(header?.chunkCount);
|
|
111
|
+
if (!Number.isInteger(frameSerial) || frameSerial < 0 || frameSerial > 0xffff_ffff
|
|
112
|
+
|| !Number.isInteger(chunkIndex) || chunkIndex < 0 || chunkIndex > 0xffff
|
|
113
|
+
|| !Number.isInteger(chunkCount) || chunkCount < 1 || chunkCount > 4096
|
|
114
|
+
|| chunkIndex >= chunkCount) {
|
|
115
|
+
throw new Error('invalid-udp-frame-chunk-header');
|
|
116
|
+
}
|
|
117
|
+
const bytes = Buffer.allocUnsafe(UDP_FRAME_CHUNK_HEADER_BYTES);
|
|
118
|
+
UDP_FRAME_CHUNK_MAGIC.copy(bytes, 0);
|
|
119
|
+
bytes.writeUInt32BE(frameSerial >>> 0, 4);
|
|
120
|
+
bytes.writeUInt16BE(chunkIndex, 8);
|
|
121
|
+
bytes.writeUInt16BE(chunkCount, 10);
|
|
122
|
+
return bytes;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function decodePacketHeader(type, bytes) {
|
|
126
|
+
if (type === 'frame'
|
|
127
|
+
&& bytes.length === UDP_FRAME_CHUNK_HEADER_BYTES
|
|
128
|
+
&& bytes[0] === UDP_FRAME_CHUNK_MAGIC[0]
|
|
129
|
+
&& bytes[1] === UDP_FRAME_CHUNK_MAGIC[1]
|
|
130
|
+
&& bytes[2] === UDP_FRAME_CHUNK_MAGIC[2]
|
|
131
|
+
&& bytes[3] === UDP_FRAME_CHUNK_MAGIC[3]) {
|
|
132
|
+
const chunkIndex = bytes.readUInt16BE(8);
|
|
133
|
+
const chunkCount = bytes.readUInt16BE(10);
|
|
134
|
+
if (chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
|
|
135
|
+
const frameSerial = bytes.readUInt32BE(4);
|
|
136
|
+
return {
|
|
137
|
+
frameId: frameSerial,
|
|
138
|
+
frameSerial,
|
|
139
|
+
chunkIndex,
|
|
140
|
+
chunkCount,
|
|
141
|
+
binaryChunkHeader: true
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
const value = JSON.parse(bytes.toString('utf8'));
|
|
146
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function decodeFrameHeader(header) {
|
|
153
|
+
if (header?.frameHeader && typeof header.frameHeader === 'object') {
|
|
154
|
+
return header.frameHeader;
|
|
155
|
+
}
|
|
156
|
+
const encoded = String(header?.frameHeaderDeflate || '');
|
|
157
|
+
if (!encoded || encoded.length > 8192 || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded)) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
const json = inflateRawSync(Buffer.from(encoded, 'base64'), { maxOutputLength: 64 * 1024 }).toString('utf8');
|
|
162
|
+
const value = JSON.parse(json);
|
|
163
|
+
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
|
|
164
|
+
} catch {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function decodeFramedPayload(payload, legacyHeader = null) {
|
|
170
|
+
const bytes = asBuffer(payload);
|
|
171
|
+
if (bytes.length < 8 || !bytes.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
|
|
172
|
+
return { header: legacyHeader || {}, payload: bytes };
|
|
173
|
+
}
|
|
174
|
+
const compressedHeaderLength = bytes.readUInt32BE(4);
|
|
175
|
+
if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
|
|
176
|
+
|| 8 + compressedHeaderLength > bytes.length) return null;
|
|
177
|
+
try {
|
|
178
|
+
const json = inflateRawSync(bytes.subarray(8, 8 + compressedHeaderLength), {
|
|
179
|
+
maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
|
|
180
|
+
}).toString('utf8');
|
|
181
|
+
const header = JSON.parse(json);
|
|
182
|
+
if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
|
|
183
|
+
return {
|
|
184
|
+
header,
|
|
185
|
+
payload: bytes.subarray(8 + compressedHeaderLength)
|
|
186
|
+
};
|
|
187
|
+
} catch {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function copyChunkRange(chunks, sourceOffset, length, target, targetOffset = 0) {
|
|
193
|
+
let remaining = length;
|
|
194
|
+
let skip = sourceOffset;
|
|
195
|
+
let written = 0;
|
|
196
|
+
for (const chunk of chunks) {
|
|
197
|
+
if (!chunk || remaining <= 0) continue;
|
|
198
|
+
if (skip >= chunk.length) {
|
|
199
|
+
skip -= chunk.length;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const available = Math.min(remaining, chunk.length - skip);
|
|
203
|
+
chunk.copy(target, targetOffset + written, skip, skip + available);
|
|
204
|
+
written += available;
|
|
205
|
+
remaining -= available;
|
|
206
|
+
skip = 0;
|
|
207
|
+
}
|
|
208
|
+
return written === length;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function decodeFramedChunks(chunks, totalBytes, legacyHeader = null) {
|
|
212
|
+
if (chunks.length === 1) {
|
|
213
|
+
const complete = decodeFramedPayload(chunks[0], legacyHeader);
|
|
214
|
+
return complete
|
|
215
|
+
? { ...complete, payloadCopyCount: 0, payloadCopyBytes: 0 }
|
|
216
|
+
: null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const prefix = Buffer.allocUnsafe(Math.min(8, totalBytes));
|
|
220
|
+
if (!copyChunkRange(chunks, 0, prefix.length, prefix)) return null;
|
|
221
|
+
if (prefix.length < 8 || !prefix.subarray(0, 4).equals(UDP_FRAME_PAYLOAD_MAGIC)) {
|
|
222
|
+
const payload = Buffer.allocUnsafe(totalBytes);
|
|
223
|
+
if (!copyChunkRange(chunks, 0, totalBytes, payload)) return null;
|
|
224
|
+
return {
|
|
225
|
+
header: legacyHeader || {},
|
|
226
|
+
payload,
|
|
227
|
+
payloadCopyCount: 1,
|
|
228
|
+
payloadCopyBytes: totalBytes
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const compressedHeaderLength = prefix.readUInt32BE(4);
|
|
233
|
+
const payloadOffset = 8 + compressedHeaderLength;
|
|
234
|
+
if (compressedHeaderLength < 1 || compressedHeaderLength > UDP_MAX_FRAME_HEADER_BYTES
|
|
235
|
+
|| payloadOffset > totalBytes) return null;
|
|
236
|
+
try {
|
|
237
|
+
const compressedHeader = Buffer.allocUnsafe(compressedHeaderLength);
|
|
238
|
+
if (!copyChunkRange(chunks, 8, compressedHeaderLength, compressedHeader)) return null;
|
|
239
|
+
const json = inflateRawSync(compressedHeader, {
|
|
240
|
+
maxOutputLength: UDP_MAX_FRAME_HEADER_BYTES
|
|
241
|
+
}).toString('utf8');
|
|
242
|
+
const header = JSON.parse(json);
|
|
243
|
+
if (!header || typeof header !== 'object' || Array.isArray(header)) return null;
|
|
244
|
+
const payloadLength = totalBytes - payloadOffset;
|
|
245
|
+
const payload = Buffer.allocUnsafe(payloadLength);
|
|
246
|
+
if (!copyChunkRange(chunks, payloadOffset, payloadLength, payload)) return null;
|
|
247
|
+
return {
|
|
248
|
+
header,
|
|
249
|
+
payload,
|
|
250
|
+
payloadCopyCount: 1,
|
|
251
|
+
payloadCopyBytes: payloadLength
|
|
252
|
+
};
|
|
253
|
+
} catch {
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function packetTypeCode(type) {
|
|
259
259
|
const code = typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()];
|
|
260
260
|
if (!code || !UDP_PACKET_TYPE_NAMES.has(code)) throw new Error('invalid-udp-packet-type');
|
|
261
261
|
return code;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function normalizeSequence(value) {
|
|
265
|
-
const sequence = Number(value);
|
|
266
|
-
if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
|
|
267
|
-
throw new Error('invalid-udp-packet-sequence');
|
|
268
|
-
}
|
|
269
|
-
return sequence;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function encodeHeader(type, header) {
|
|
273
|
-
if ((typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()])
|
|
274
|
-
=== UDP_PACKET_TYPES.frame
|
|
275
|
-
&& Number.isInteger(Number(header?.frameSerial))) {
|
|
276
|
-
return encodeFrameChunkHeader(header);
|
|
277
|
-
}
|
|
278
|
-
const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
|
|
279
|
-
if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
|
|
280
|
-
return bytes;
|
|
281
|
-
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function normalizeSequence(value) {
|
|
265
|
+
const sequence = Number(value);
|
|
266
|
+
if (!Number.isInteger(sequence) || sequence < 0 || sequence >= UDP_SERIAL_MODULUS) {
|
|
267
|
+
throw new Error('invalid-udp-packet-sequence');
|
|
268
|
+
}
|
|
269
|
+
return sequence;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function encodeHeader(type, header) {
|
|
273
|
+
if ((typeof type === 'number' ? type : UDP_PACKET_TYPES[String(type || '').toLowerCase()])
|
|
274
|
+
=== UDP_PACKET_TYPES.frame
|
|
275
|
+
&& Number.isInteger(Number(header?.frameSerial))) {
|
|
276
|
+
return encodeFrameChunkHeader(header);
|
|
277
|
+
}
|
|
278
|
+
const bytes = Buffer.from(JSON.stringify(header && typeof header === 'object' ? header : {}), 'utf8');
|
|
279
|
+
if (bytes.length > UDP_MAX_HEADER_BYTES) throw new Error('udp-packet-header-too-large');
|
|
280
|
+
return bytes;
|
|
281
|
+
}
|
|
282
282
|
|
|
283
283
|
export function deriveUdpSessionKey(keyMaterial) {
|
|
284
284
|
return normalizeKey(keyMaterial);
|
|
@@ -288,22 +288,22 @@ export function createUdpSessionKey() {
|
|
|
288
288
|
return crypto.randomBytes(32);
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
|
|
292
|
-
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
293
|
-
const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
|
|
294
|
-
const headerBytes = encodeHeader(type, header);
|
|
295
|
-
const payloadBytes = asBuffer(payload);
|
|
296
|
-
if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
|
|
297
|
-
|
|
298
|
-
const preamble = Buffer.allocUnsafe(UDP_FIXED_HEADER_BYTES);
|
|
299
|
-
UDP_PACKET_MAGIC.copy(preamble, 0);
|
|
291
|
+
export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {}, payload = Buffer.alloc(0) }) {
|
|
292
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
293
|
+
const sessionBytes = Buffer.from(normalizedSessionId, 'utf8');
|
|
294
|
+
const headerBytes = encodeHeader(type, header);
|
|
295
|
+
const payloadBytes = asBuffer(payload);
|
|
296
|
+
if (payloadBytes.length > UDP_MAX_PAYLOAD_BYTES) throw new Error('udp-packet-payload-too-large');
|
|
297
|
+
|
|
298
|
+
const preamble = Buffer.allocUnsafe(UDP_FIXED_HEADER_BYTES);
|
|
299
|
+
UDP_PACKET_MAGIC.copy(preamble, 0);
|
|
300
300
|
preamble[4] = UDP_P2P_VERSION;
|
|
301
301
|
preamble[5] = packetTypeCode(type);
|
|
302
302
|
preamble[6] = sessionBytes.length;
|
|
303
303
|
preamble.writeUInt16BE(headerBytes.length, 7);
|
|
304
304
|
preamble.writeUInt16BE(payloadBytes.length, 9);
|
|
305
|
-
preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
|
|
306
|
-
const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
|
|
305
|
+
preamble.writeUInt32BE((Number(sequence) >>> 0), 11);
|
|
306
|
+
const nonce = crypto.randomBytes(UDP_NONCE_BYTES);
|
|
307
307
|
const associatedData = Buffer.concat([preamble, nonce, sessionBytes, headerBytes]);
|
|
308
308
|
const cipher = crypto.createCipheriv('aes-256-gcm', normalizeKey(key), nonce);
|
|
309
309
|
cipher.setAAD(associatedData);
|
|
@@ -314,10 +314,10 @@ export function encodeUdpPacket({ type, sessionId, key, sequence = 0, header = {
|
|
|
314
314
|
return packet;
|
|
315
315
|
}
|
|
316
316
|
|
|
317
|
-
export function decodeUdpPacket(packet, key, expectedSession = null) {
|
|
318
|
-
const bytes = asBuffer(packet);
|
|
319
|
-
if (!isUdpBinaryDatagram(bytes)) return null;
|
|
320
|
-
const type = UDP_PACKET_TYPE_NAMES.get(bytes[5]);
|
|
317
|
+
export function decodeUdpPacket(packet, key, expectedSession = null) {
|
|
318
|
+
const bytes = asBuffer(packet);
|
|
319
|
+
if (!isUdpBinaryDatagram(bytes)) return null;
|
|
320
|
+
const type = UDP_PACKET_TYPE_NAMES.get(bytes[5]);
|
|
321
321
|
const sessionLength = bytes[6];
|
|
322
322
|
const headerLength = bytes.readUInt16BE(7);
|
|
323
323
|
const payloadLength = bytes.readUInt16BE(9);
|
|
@@ -328,204 +328,204 @@ export function decodeUdpPacket(packet, key, expectedSession = null) {
|
|
|
328
328
|
|| headerLength > UDP_MAX_HEADER_BYTES || payloadLength > UDP_MAX_PAYLOAD_BYTES
|
|
329
329
|
|| expectedLength !== bytes.length) return null;
|
|
330
330
|
|
|
331
|
-
const associatedData = bytes.subarray(0, associatedLength);
|
|
332
|
-
const sessionStart = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES;
|
|
333
|
-
const sessionSlice = bytes.subarray(sessionStart, sessionStart + sessionLength);
|
|
334
|
-
const expectedSessionBytes = Buffer.isBuffer(expectedSession?.bytes)
|
|
335
|
-
? expectedSession.bytes
|
|
336
|
-
: null;
|
|
337
|
-
if (expectedSessionBytes && !sessionSlice.equals(expectedSessionBytes)) return null;
|
|
338
|
-
const sessionId = expectedSessionBytes
|
|
339
|
-
? String(expectedSession?.id || '')
|
|
340
|
-
: sessionSlice.toString('utf8');
|
|
341
|
-
const headerStart = sessionStart + sessionLength;
|
|
342
|
-
const nonce = bytes.subarray(UDP_FIXED_HEADER_BYTES, UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES);
|
|
343
|
-
const ciphertextStart = associatedLength;
|
|
344
|
-
const ciphertext = bytes.subarray(ciphertextStart, ciphertextStart + payloadLength);
|
|
345
|
-
const tag = bytes.subarray(ciphertextStart + payloadLength);
|
|
346
|
-
try {
|
|
347
|
-
const decipher = crypto.createDecipheriv('aes-256-gcm', normalizeKey(key), nonce);
|
|
348
|
-
decipher.setAAD(associatedData);
|
|
349
|
-
decipher.setAuthTag(tag);
|
|
350
|
-
let payload = decipher.update(ciphertext);
|
|
351
|
-
const final = decipher.final();
|
|
352
|
-
if (final.length > 0) payload = Buffer.concat([payload, final], payload.length + final.length);
|
|
353
|
-
const header = decodePacketHeader(
|
|
354
|
-
type,
|
|
355
|
-
bytes.subarray(headerStart, headerStart + headerLength)
|
|
356
|
-
);
|
|
357
|
-
if (!header) return null;
|
|
358
|
-
return { type, sessionId, sequence, header, payload };
|
|
359
|
-
} catch {
|
|
360
|
-
return null;
|
|
331
|
+
const associatedData = bytes.subarray(0, associatedLength);
|
|
332
|
+
const sessionStart = UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES;
|
|
333
|
+
const sessionSlice = bytes.subarray(sessionStart, sessionStart + sessionLength);
|
|
334
|
+
const expectedSessionBytes = Buffer.isBuffer(expectedSession?.bytes)
|
|
335
|
+
? expectedSession.bytes
|
|
336
|
+
: null;
|
|
337
|
+
if (expectedSessionBytes && !sessionSlice.equals(expectedSessionBytes)) return null;
|
|
338
|
+
const sessionId = expectedSessionBytes
|
|
339
|
+
? String(expectedSession?.id || '')
|
|
340
|
+
: sessionSlice.toString('utf8');
|
|
341
|
+
const headerStart = sessionStart + sessionLength;
|
|
342
|
+
const nonce = bytes.subarray(UDP_FIXED_HEADER_BYTES, UDP_FIXED_HEADER_BYTES + UDP_NONCE_BYTES);
|
|
343
|
+
const ciphertextStart = associatedLength;
|
|
344
|
+
const ciphertext = bytes.subarray(ciphertextStart, ciphertextStart + payloadLength);
|
|
345
|
+
const tag = bytes.subarray(ciphertextStart + payloadLength);
|
|
346
|
+
try {
|
|
347
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', normalizeKey(key), nonce);
|
|
348
|
+
decipher.setAAD(associatedData);
|
|
349
|
+
decipher.setAuthTag(tag);
|
|
350
|
+
let payload = decipher.update(ciphertext);
|
|
351
|
+
const final = decipher.final();
|
|
352
|
+
if (final.length > 0) payload = Buffer.concat([payload, final], payload.length + final.length);
|
|
353
|
+
const header = decodePacketHeader(
|
|
354
|
+
type,
|
|
355
|
+
bytes.subarray(headerStart, headerStart + headerLength)
|
|
356
|
+
);
|
|
357
|
+
if (!header) return null;
|
|
358
|
+
return { type, sessionId, sequence, header, payload };
|
|
359
|
+
} catch {
|
|
360
|
+
return null;
|
|
361
361
|
}
|
|
362
362
|
}
|
|
363
363
|
|
|
364
|
-
export class UdpReplayWindow {
|
|
364
|
+
export class UdpReplayWindow {
|
|
365
365
|
constructor(size = 512) {
|
|
366
366
|
this.size = Math.max(32, Math.min(4096, Math.floor(Number(size) || 512)));
|
|
367
367
|
this.highest = -1;
|
|
368
368
|
this.seen = new Set();
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
accept(sequence) {
|
|
372
|
-
let value;
|
|
373
|
-
try {
|
|
374
|
-
value = normalizeSequence(sequence);
|
|
375
|
-
} catch {
|
|
376
|
-
return false;
|
|
377
|
-
}
|
|
378
|
-
if (this.seen.has(value)) return false;
|
|
379
|
-
if (this.highest >= 0) {
|
|
380
|
-
const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
381
|
-
if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
|
|
382
|
-
if (forward > UDP_SERIAL_HALF_RANGE) {
|
|
383
|
-
const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
384
|
-
if (age > this.size) return false;
|
|
385
|
-
} else {
|
|
386
|
-
this.highest = value;
|
|
387
|
-
}
|
|
388
|
-
} else {
|
|
389
|
-
this.highest = value;
|
|
390
|
-
}
|
|
391
|
-
this.seen.add(value);
|
|
392
|
-
for (const item of this.seen) {
|
|
393
|
-
const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
394
|
-
if (age > this.size) this.seen.delete(item);
|
|
395
|
-
}
|
|
396
|
-
return true;
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export class UdpFrameReassembler {
|
|
401
|
-
constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
|
|
402
|
-
this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
|
|
403
|
-
this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
|
|
404
|
-
this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
|
|
405
|
-
this.pending = new Map();
|
|
406
|
-
this.pendingBytes = 0;
|
|
407
|
-
this.pendingChunkBuffers = 0;
|
|
408
|
-
this.pendingBytesHighWatermark = 0;
|
|
409
|
-
this.pendingChunkBuffersHighWatermark = 0;
|
|
410
|
-
this.droppedFrames = 0;
|
|
411
|
-
this.expiredFrames = 0;
|
|
412
|
-
this.evictedFrames = 0;
|
|
413
|
-
this.completedFrames = 0;
|
|
414
|
-
this.completedPayloadCopyCount = 0;
|
|
415
|
-
this.completedPayloadCopyBytes = 0;
|
|
416
|
-
this.fullWireDuplicateCopyCount = 0;
|
|
417
|
-
this.binaryChunkHeaderCount = 0;
|
|
418
|
-
this.legacyChunkHeaderCount = 0;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
ingest(packet, now = Date.now()) {
|
|
422
|
-
if (!packet || packet.type !== 'frame') return null;
|
|
423
|
-
this.expire(now);
|
|
424
|
-
const header = packet.header && typeof packet.header === 'object' ? packet.header : {};
|
|
425
|
-
const frameId = Number.isInteger(header.frameSerial)
|
|
426
|
-
? header.frameSerial
|
|
427
|
-
: String(header.frameId || '').slice(0, 160);
|
|
428
|
-
const chunkIndex = Number(header.chunkIndex);
|
|
429
|
-
const chunkCount = Number(header.chunkCount);
|
|
430
|
-
if ((frameId === '' || frameId === null) || !Number.isInteger(chunkIndex) || !Number.isInteger(chunkCount)
|
|
431
|
-
|| chunkIndex < 0 || chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
|
|
432
|
-
let state = this.pending.get(frameId);
|
|
433
|
-
if (!state) {
|
|
434
|
-
while (this.pending.size >= this.maxPending) {
|
|
435
|
-
const oldest = this.pending.keys().next().value;
|
|
436
|
-
if (oldest === undefined) break;
|
|
437
|
-
const evicted = this.#detach(oldest);
|
|
438
|
-
if (evicted) {
|
|
439
|
-
this.droppedFrames += 1;
|
|
440
|
-
this.evictedFrames += 1;
|
|
441
|
-
}
|
|
442
|
-
}
|
|
371
|
+
accept(sequence) {
|
|
372
|
+
let value;
|
|
373
|
+
try {
|
|
374
|
+
value = normalizeSequence(sequence);
|
|
375
|
+
} catch {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
if (this.seen.has(value)) return false;
|
|
379
|
+
if (this.highest >= 0) {
|
|
380
|
+
const forward = (value - this.highest + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
381
|
+
if (forward === 0 || forward === UDP_SERIAL_HALF_RANGE) return false;
|
|
382
|
+
if (forward > UDP_SERIAL_HALF_RANGE) {
|
|
383
|
+
const age = (this.highest - value + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
384
|
+
if (age > this.size) return false;
|
|
385
|
+
} else {
|
|
386
|
+
this.highest = value;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
this.highest = value;
|
|
390
|
+
}
|
|
391
|
+
this.seen.add(value);
|
|
392
|
+
for (const item of this.seen) {
|
|
393
|
+
const age = (this.highest - item + UDP_SERIAL_MODULUS) % UDP_SERIAL_MODULUS;
|
|
394
|
+
if (age > this.size) this.seen.delete(item);
|
|
395
|
+
}
|
|
396
|
+
return true;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
export class UdpFrameReassembler {
|
|
401
|
+
constructor({ maxPending = 2, timeoutMs = 500, maxFrameBytes = 8 * 1024 * 1024 } = {}) {
|
|
402
|
+
this.maxPending = Math.max(1, Math.min(16, Math.floor(Number(maxPending) || 2)));
|
|
403
|
+
this.timeoutMs = Math.max(20, Math.min(2000, Math.floor(Number(timeoutMs) || 500)));
|
|
404
|
+
this.maxFrameBytes = Math.max(64 * 1024, Math.min(16 * 1024 * 1024, Math.floor(Number(maxFrameBytes) || 8 * 1024 * 1024)));
|
|
405
|
+
this.pending = new Map();
|
|
406
|
+
this.pendingBytes = 0;
|
|
407
|
+
this.pendingChunkBuffers = 0;
|
|
408
|
+
this.pendingBytesHighWatermark = 0;
|
|
409
|
+
this.pendingChunkBuffersHighWatermark = 0;
|
|
410
|
+
this.droppedFrames = 0;
|
|
411
|
+
this.expiredFrames = 0;
|
|
412
|
+
this.evictedFrames = 0;
|
|
413
|
+
this.completedFrames = 0;
|
|
414
|
+
this.completedPayloadCopyCount = 0;
|
|
415
|
+
this.completedPayloadCopyBytes = 0;
|
|
416
|
+
this.fullWireDuplicateCopyCount = 0;
|
|
417
|
+
this.binaryChunkHeaderCount = 0;
|
|
418
|
+
this.legacyChunkHeaderCount = 0;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
ingest(packet, now = Date.now()) {
|
|
422
|
+
if (!packet || packet.type !== 'frame') return null;
|
|
423
|
+
this.expire(now);
|
|
424
|
+
const header = packet.header && typeof packet.header === 'object' ? packet.header : {};
|
|
425
|
+
const frameId = Number.isInteger(header.frameSerial)
|
|
426
|
+
? header.frameSerial
|
|
427
|
+
: String(header.frameId || '').slice(0, 160);
|
|
428
|
+
const chunkIndex = Number(header.chunkIndex);
|
|
429
|
+
const chunkCount = Number(header.chunkCount);
|
|
430
|
+
if ((frameId === '' || frameId === null) || !Number.isInteger(chunkIndex) || !Number.isInteger(chunkCount)
|
|
431
|
+
|| chunkIndex < 0 || chunkCount < 1 || chunkCount > 4096 || chunkIndex >= chunkCount) return null;
|
|
432
|
+
let state = this.pending.get(frameId);
|
|
433
|
+
if (!state) {
|
|
434
|
+
while (this.pending.size >= this.maxPending) {
|
|
435
|
+
const oldest = this.pending.keys().next().value;
|
|
436
|
+
if (oldest === undefined) break;
|
|
437
|
+
const evicted = this.#detach(oldest);
|
|
438
|
+
if (evicted) {
|
|
439
|
+
this.droppedFrames += 1;
|
|
440
|
+
this.evictedFrames += 1;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
443
|
state = { createdAt: now, chunkCount, chunks: new Array(chunkCount), received: 0, totalBytes: 0, frameHeader: null };
|
|
444
444
|
this.pending.set(frameId, state);
|
|
445
|
-
}
|
|
446
|
-
if (state.chunkCount !== chunkCount || state.chunks[chunkIndex]) return null;
|
|
447
|
-
const payload = asBuffer(packet.payload);
|
|
448
|
-
state.chunks[chunkIndex] = payload;
|
|
449
|
-
state.received += 1;
|
|
450
|
-
state.totalBytes += payload.length;
|
|
451
|
-
this.pendingBytes += payload.length;
|
|
452
|
-
this.pendingChunkBuffers += 1;
|
|
453
|
-
this.pendingBytesHighWatermark = Math.max(this.pendingBytesHighWatermark, this.pendingBytes);
|
|
454
|
-
this.pendingChunkBuffersHighWatermark = Math.max(
|
|
455
|
-
this.pendingChunkBuffersHighWatermark,
|
|
456
|
-
this.pendingChunkBuffers
|
|
457
|
-
);
|
|
458
|
-
if (header.binaryChunkHeader === true) this.binaryChunkHeaderCount += 1;
|
|
459
|
-
else this.legacyChunkHeaderCount += 1;
|
|
460
|
-
const decodedFrameHeader = decodeFrameHeader(header);
|
|
461
|
-
if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
|
|
462
|
-
if (state.totalBytes > this.maxFrameBytes) {
|
|
463
|
-
if (this.#detach(frameId)) this.droppedFrames += 1;
|
|
464
|
-
return null;
|
|
465
|
-
}
|
|
466
|
-
if (state.received !== state.chunkCount) return null;
|
|
467
|
-
this.#detach(frameId);
|
|
468
|
-
const complete = decodeFramedChunks(
|
|
469
|
-
state.chunks,
|
|
470
|
-
state.totalBytes,
|
|
471
|
-
state.frameHeader || header.frameHeader || {}
|
|
472
|
-
);
|
|
473
|
-
if (!complete) {
|
|
474
|
-
this.droppedFrames += 1;
|
|
475
|
-
return null;
|
|
476
|
-
}
|
|
477
|
-
this.completedFrames += 1;
|
|
478
|
-
this.completedPayloadCopyCount += complete.payloadCopyCount;
|
|
479
|
-
this.completedPayloadCopyBytes += complete.payloadCopyBytes;
|
|
480
|
-
return {
|
|
481
|
-
frameId,
|
|
482
|
-
header: complete.header,
|
|
483
|
-
payload: complete.payload
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
expire(now = Date.now()) {
|
|
488
|
-
for (const [frameId, state] of this.pending) {
|
|
489
|
-
if (now - state.createdAt > this.timeoutMs && this.#detach(frameId)) {
|
|
490
|
-
this.droppedFrames += 1;
|
|
491
|
-
this.expiredFrames += 1;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
clear() {
|
|
497
|
-
this.pending.clear();
|
|
498
|
-
this.pendingBytes = 0;
|
|
499
|
-
this.pendingChunkBuffers = 0;
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
getStats() {
|
|
503
|
-
return {
|
|
504
|
-
pendingFrames: this.pending.size,
|
|
505
|
-
pendingBytes: this.pendingBytes,
|
|
506
|
-
pendingChunkBuffers: this.pendingChunkBuffers,
|
|
507
|
-
pendingBytesHighWatermark: this.pendingBytesHighWatermark,
|
|
508
|
-
pendingChunkBuffersHighWatermark: this.pendingChunkBuffersHighWatermark,
|
|
509
|
-
droppedFrames: this.droppedFrames,
|
|
510
|
-
expiredFrames: this.expiredFrames,
|
|
511
|
-
evictedFrames: this.evictedFrames,
|
|
512
|
-
completedFrames: this.completedFrames,
|
|
513
|
-
completedPayloadCopyCount: this.completedPayloadCopyCount,
|
|
514
|
-
completedPayloadCopyBytes: this.completedPayloadCopyBytes,
|
|
515
|
-
fullWireDuplicateCopyCount: this.fullWireDuplicateCopyCount,
|
|
516
|
-
binaryChunkHeaderCount: this.binaryChunkHeaderCount,
|
|
517
|
-
legacyChunkHeaderCount: this.legacyChunkHeaderCount
|
|
518
|
-
};
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
#detach(frameId) {
|
|
522
|
-
const state = this.pending.get(frameId);
|
|
523
|
-
if (!state || !this.pending.delete(frameId)) return null;
|
|
524
|
-
this.pendingBytes = Math.max(0, this.pendingBytes - state.totalBytes);
|
|
525
|
-
this.pendingChunkBuffers = Math.max(0, this.pendingChunkBuffers - state.received);
|
|
526
|
-
return state;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
445
|
+
}
|
|
446
|
+
if (state.chunkCount !== chunkCount || state.chunks[chunkIndex]) return null;
|
|
447
|
+
const payload = asBuffer(packet.payload);
|
|
448
|
+
state.chunks[chunkIndex] = payload;
|
|
449
|
+
state.received += 1;
|
|
450
|
+
state.totalBytes += payload.length;
|
|
451
|
+
this.pendingBytes += payload.length;
|
|
452
|
+
this.pendingChunkBuffers += 1;
|
|
453
|
+
this.pendingBytesHighWatermark = Math.max(this.pendingBytesHighWatermark, this.pendingBytes);
|
|
454
|
+
this.pendingChunkBuffersHighWatermark = Math.max(
|
|
455
|
+
this.pendingChunkBuffersHighWatermark,
|
|
456
|
+
this.pendingChunkBuffers
|
|
457
|
+
);
|
|
458
|
+
if (header.binaryChunkHeader === true) this.binaryChunkHeaderCount += 1;
|
|
459
|
+
else this.legacyChunkHeaderCount += 1;
|
|
460
|
+
const decodedFrameHeader = decodeFrameHeader(header);
|
|
461
|
+
if (decodedFrameHeader) state.frameHeader = decodedFrameHeader;
|
|
462
|
+
if (state.totalBytes > this.maxFrameBytes) {
|
|
463
|
+
if (this.#detach(frameId)) this.droppedFrames += 1;
|
|
464
|
+
return null;
|
|
465
|
+
}
|
|
466
|
+
if (state.received !== state.chunkCount) return null;
|
|
467
|
+
this.#detach(frameId);
|
|
468
|
+
const complete = decodeFramedChunks(
|
|
469
|
+
state.chunks,
|
|
470
|
+
state.totalBytes,
|
|
471
|
+
state.frameHeader || header.frameHeader || {}
|
|
472
|
+
);
|
|
473
|
+
if (!complete) {
|
|
474
|
+
this.droppedFrames += 1;
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
this.completedFrames += 1;
|
|
478
|
+
this.completedPayloadCopyCount += complete.payloadCopyCount;
|
|
479
|
+
this.completedPayloadCopyBytes += complete.payloadCopyBytes;
|
|
480
|
+
return {
|
|
481
|
+
frameId,
|
|
482
|
+
header: complete.header,
|
|
483
|
+
payload: complete.payload
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
expire(now = Date.now()) {
|
|
488
|
+
for (const [frameId, state] of this.pending) {
|
|
489
|
+
if (now - state.createdAt > this.timeoutMs && this.#detach(frameId)) {
|
|
490
|
+
this.droppedFrames += 1;
|
|
491
|
+
this.expiredFrames += 1;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
clear() {
|
|
497
|
+
this.pending.clear();
|
|
498
|
+
this.pendingBytes = 0;
|
|
499
|
+
this.pendingChunkBuffers = 0;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
getStats() {
|
|
503
|
+
return {
|
|
504
|
+
pendingFrames: this.pending.size,
|
|
505
|
+
pendingBytes: this.pendingBytes,
|
|
506
|
+
pendingChunkBuffers: this.pendingChunkBuffers,
|
|
507
|
+
pendingBytesHighWatermark: this.pendingBytesHighWatermark,
|
|
508
|
+
pendingChunkBuffersHighWatermark: this.pendingChunkBuffersHighWatermark,
|
|
509
|
+
droppedFrames: this.droppedFrames,
|
|
510
|
+
expiredFrames: this.expiredFrames,
|
|
511
|
+
evictedFrames: this.evictedFrames,
|
|
512
|
+
completedFrames: this.completedFrames,
|
|
513
|
+
completedPayloadCopyCount: this.completedPayloadCopyCount,
|
|
514
|
+
completedPayloadCopyBytes: this.completedPayloadCopyBytes,
|
|
515
|
+
fullWireDuplicateCopyCount: this.fullWireDuplicateCopyCount,
|
|
516
|
+
binaryChunkHeaderCount: this.binaryChunkHeaderCount,
|
|
517
|
+
legacyChunkHeaderCount: this.legacyChunkHeaderCount
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
#detach(frameId) {
|
|
522
|
+
const state = this.pending.get(frameId);
|
|
523
|
+
if (!state || !this.pending.delete(frameId)) return null;
|
|
524
|
+
this.pendingBytes = Math.max(0, this.pendingBytes - state.totalBytes);
|
|
525
|
+
this.pendingChunkBuffers = Math.max(0, this.pendingChunkBuffers - state.received);
|
|
526
|
+
return state;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
529
|
|
|
530
530
|
export function encodeRendezvousMessage(message) {
|
|
531
531
|
const payload = Buffer.from(JSON.stringify(message && typeof message === 'object' ? message : {}), 'utf8');
|