quico 0.1.3 → 0.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.
package/src/h3.js ADDED
@@ -0,0 +1,911 @@
1
+ /*
2
+ * quico: HTTP/3 and QUIC implementation for Node.js
3
+ * Copyright 2025 colocohen
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at:
8
+ *
9
+ * https://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * This file is part of the open-source project hosted at:
12
+ * https://github.com/colocohen/quico
13
+ */
14
+
15
+ import {
16
+ Emitter,
17
+ concatUint8Arrays,
18
+ writeVarInt,
19
+ readVarInt
20
+ } from './utils.js';
21
+
22
+
23
+ // ============================================================
24
+ // QPACK Static Table (RFC 9204 Appendix A)
25
+ // ============================================================
26
+
27
+ var QPACK_STATIC = [
28
+ [':authority',''], [':path','/'], ['age','0'], ['content-disposition',''],
29
+ ['content-length','0'], ['cookie',''], ['date',''], ['etag',''],
30
+ ['if-modified-since',''], ['if-none-match',''], ['last-modified',''],
31
+ ['link',''], ['location',''], ['referer',''], ['set-cookie',''],
32
+ [':method','CONNECT'], [':method','DELETE'], [':method','GET'],
33
+ [':method','HEAD'], [':method','OPTIONS'], [':method','POST'],
34
+ [':method','PUT'], [':scheme','http'], [':scheme','https'],
35
+ [':status','103'], [':status','200'], [':status','304'],
36
+ [':status','404'], [':status','503'], ['accept','*/*'],
37
+ ['accept','application/dns-message'], ['accept-encoding','gzip, deflate, br'],
38
+ ['accept-ranges','bytes'], ['access-control-allow-headers','cache-control'],
39
+ ['access-control-allow-headers','content-type'],
40
+ ['access-control-allow-origin','*'], ['cache-control','max-age=0'],
41
+ ['cache-control','max-age=2592000'], ['cache-control','max-age=604800'],
42
+ ['cache-control','no-cache'], ['cache-control','no-store'],
43
+ ['cache-control','public, max-age=31536000'], ['content-encoding','br'],
44
+ ['content-encoding','gzip'], ['content-type','application/dns-message'],
45
+ ['content-type','application/javascript'], ['content-type','application/json'],
46
+ ['content-type','application/x-www-form-urlencoded'],
47
+ ['content-type','image/gif'], ['content-type','image/jpeg'],
48
+ ['content-type','image/png'], ['content-type','text/css'],
49
+ ['content-type','text/html; charset=utf-8'], ['content-type','text/plain'],
50
+ ['content-type','text/plain;charset=utf-8'], ['range','bytes=0-'],
51
+ ['strict-transport-security','max-age=31536000'],
52
+ ['strict-transport-security','max-age=31536000; includesubdomains'],
53
+ ['strict-transport-security','max-age=31536000; includesubdomains; preload'],
54
+ ['vary','accept-encoding'], ['vary','origin'],
55
+ ['x-content-type-options','nosniff'], ['x-xss-protection','1; mode=block'],
56
+ [':status','100'], [':status','204'], [':status','206'], [':status','302'],
57
+ [':status','400'], [':status','403'], [':status','421'], [':status','425'],
58
+ [':status','500'], ['accept-language',''],
59
+ ['access-control-allow-credentials','FALSE'],
60
+ ['access-control-allow-credentials','TRUE'],
61
+ ['access-control-allow-headers','*'],
62
+ ['access-control-allow-methods','get'],
63
+ ['access-control-allow-methods','get, post, options'],
64
+ ['access-control-allow-methods','options'],
65
+ ['access-control-expose-headers','content-length'],
66
+ ['access-control-request-headers','content-type'],
67
+ ['access-control-request-method','get'],
68
+ ['access-control-request-method','post'], ['alt-svc','clear'],
69
+ ['authorization',''],
70
+ ['content-security-policy',"script-src 'none'; object-src 'none'; base-uri 'none'"],
71
+ ['early-data','1'], ['expect-ct',''], ['forwarded',''], ['if-range',''],
72
+ ['origin',''], ['purpose','prefetch'], ['server',''],
73
+ ['timing-allow-origin','*'], ['upgrade-insecure-requests','1'],
74
+ ['user-agent',''], ['x-forwarded-for',''],
75
+ ['x-frame-options','deny'], ['x-frame-options','sameorigin']
76
+ ];
77
+
78
+
79
+ // ============================================================
80
+ // Huffman (RFC 7541 Appendix B)
81
+ // ============================================================
82
+
83
+ var HUFF_CODES = new Uint32Array([
84
+ 0x1ff8,0x7fffd8,0xfffffe2,0xfffffe3,0xfffffe4,0xfffffe5,0xfffffe6,0xfffffe7,
85
+ 0xfffffe8,0xffffea,0x3ffffffc,0xfffffe9,0xfffffea,0x3ffffffd,0xfffffeb,0xfffffec,
86
+ 0xfffffed,0xfffffee,0xfffffef,0xffffff0,0xffffff1,0xffffff2,0x3ffffffe,0xffffff3,
87
+ 0xffffff4,0xffffff5,0xffffff6,0xffffff7,0xffffff8,0xffffff9,0xffffffa,0xffffffb,
88
+ 0x14,0x3f8,0x3f9,0xffa,0x1ff9,0x15,0xf8,0x7fa,0x3fa,0x3fb,0xf9,0x7fb,0xfa,
89
+ 0x16,0x17,0x18,0x0,0x1,0x2,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x5c,0xfb,
90
+ 0x7ffc,0x20,0xffb,0x3fc,0x1ffa,0x21,0x5d,0x5e,0x5f,0x60,0x61,0x62,0x63,0x64,
91
+ 0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xfc,
92
+ 0x73,0xfd,0x1ffb,0x7fff0,0x1ffc,0x3ffc,0x22,0x7ffd,0x3,0x23,0x4,0x24,0x5,0x25,
93
+ 0x26,0x27,0x6,0x74,0x75,0x28,0x29,0x2a,0x7,0x2b,0x76,0x2c,0x8,0x9,0x2d,0x77,
94
+ 0x78,0x79,0x7a,0x7b,0x7ffe,0x7fc,0x3ffd,0x1ffd,0xffffffc,0xfffe6,0x3fffd2,
95
+ 0xfffe7,0xfffe8,0x3fffd3,0x3fffd4,0x3fffd5,0x7fffd9,0x3fffd6,0x7fffda,0x7fffdb,
96
+ 0x7fffdc,0x7fffdd,0x7fffde,0xffffeb,0x7fffdf,0xffffec,0xffffed,0x3fffd7,0x7fffe0,
97
+ 0xffffee,0x7fffe1,0x7fffe2,0x7fffe3,0x7fffe4,0x1fffdc,0x3fffd8,0x7fffe5,0x3fffd9,
98
+ 0x7fffe6,0x7fffe7,0xffffef,0x3fffda,0x1fffdd,0xfffe9,0x3fffdb,0x3fffdc,0x7fffe8,
99
+ 0x7fffe9,0x1fffde,0x7fffea,0x3fffdd,0x3fffde,0xfffff0,0x1fffdf,0x3fffdf,0x7fffeb,
100
+ 0x7fffec,0x1fffe0,0x1fffe1,0x3fffe0,0x1fffe2,0x7fffed,0x3fffe1,0x7fffee,0x7fffef,
101
+ 0xfffea,0x3fffe2,0x3fffe3,0x3fffe4,0x7ffff0,0x3fffe5,0x3fffe6,0x7ffff1,0x3ffffe0,
102
+ 0x3ffffe1,0xfffeb,0x7fff1,0x3fffe7,0x7ffff2,0x3fffe8,0x1ffffec,0x3ffffe2,0x3ffffe3,
103
+ 0x3ffffe4,0x7ffffde,0x7ffffdf,0x3ffffe5,0xfffff1,0x1ffffed,0x7fff2,0x1fffe3,
104
+ 0x3ffffe6,0x7ffffe0,0x7ffffe1,0x3ffffe7,0x7ffffe2,0xfffff2,0x1fffe4,0x1fffe5,
105
+ 0x3ffffe8,0x3ffffe9,0xffffffd,0x7ffffe3,0x7ffffe4,0x7ffffe5,0xfffec,0xfffff3,
106
+ 0xfffed,0x1fffe6,0x3fffe9,0x1fffe7,0x1fffe8,0x7ffff3,0x3fffea,0x3fffeb,0x1ffffee,
107
+ 0x1ffffef,0xfffff4,0xfffff5,0x3ffffea,0x7ffff4,0x3ffffeb,0x7ffffe6,0x3ffffec,
108
+ 0x3ffffed,0x7ffffe7,0x7ffffe8,0x7ffffe9,0x7ffffea,0x7ffffeb,0xffffffe,0x7ffffec,
109
+ 0x7ffffed,0x7ffffee,0x7ffffef,0x7fffff0,0x3ffffee,0x3fffffff
110
+ ]);
111
+
112
+ var HUFF_BITS = new Uint8Array([
113
+ 13,23,28,28,28,28,28,28,28,24,30,28,28,30,28,28,28,28,28,28,28,28,30,28,28,
114
+ 28,28,28,28,28,28,28,6,10,10,12,13,6,8,11,10,10,8,11,8,6,6,6,5,5,5,6,6,6,6,
115
+ 6,6,6,7,8,15,6,12,10,13,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,7,
116
+ 8,13,19,13,14,6,15,5,6,5,6,5,6,6,6,5,7,7,6,6,6,5,6,7,6,5,5,6,7,7,7,7,7,15,
117
+ 11,14,13,28,20,22,20,20,22,22,22,23,22,23,23,23,23,23,24,23,24,24,22,23,24,
118
+ 23,23,23,23,21,22,23,22,23,23,24,22,21,20,22,22,23,23,21,23,22,22,24,21,22,
119
+ 23,23,21,21,22,21,23,22,23,23,20,22,22,22,23,22,22,23,26,26,20,19,22,23,22,
120
+ 25,26,26,26,27,27,26,24,25,19,21,26,27,27,26,27,24,21,21,26,26,28,27,27,27,
121
+ 20,24,20,21,22,21,21,23,22,22,25,25,24,24,26,23,26,27,26,26,27,27,27,27,27,
122
+ 28,27,27,27,27,27,26,30
123
+ ]);
124
+
125
+ var _huffRoot = (function () {
126
+ var root = {};
127
+ for (var i = 0; i < HUFF_CODES.length; i++) {
128
+ var code = HUFF_CODES[i], bits = HUFF_BITS[i], node = root;
129
+ for (var j = bits - 1; j >= 0; j--) {
130
+ var bit = (code >> j) & 1;
131
+ if (!node[bit]) node[bit] = {};
132
+ node = node[bit];
133
+ }
134
+ node.sym = i;
135
+ }
136
+ return root;
137
+ })();
138
+
139
+ function huffDecode(buf) {
140
+ var out = [], node = _huffRoot, nbits = 0, current = 0;
141
+ for (var i = 0; i < buf.length; i++) {
142
+ current = (current << 8) | buf[i];
143
+ nbits += 8;
144
+ while (nbits > 0) {
145
+ node = node[(current >> (nbits - 1)) & 1];
146
+ if (!node) throw new Error('Invalid Huffman');
147
+ nbits--;
148
+ if (node.sym !== undefined) { out.push(node.sym); node = _huffRoot; }
149
+ }
150
+ }
151
+ return new TextDecoder().decode(Uint8Array.from(out));
152
+ }
153
+
154
+
155
+ // ============================================================
156
+ // QPACK parse/build helpers
157
+ // ============================================================
158
+
159
+ function decodeQpackVarInt(buf, prefixBits, pos) {
160
+ var max = (1 << prefixBits) - 1;
161
+ var value = buf[pos] & max;
162
+ pos++;
163
+ if (value < max) return { value: value, next: pos };
164
+ var m = 0;
165
+ while (true) {
166
+ var byte = buf[pos++];
167
+ value += (byte & 0x7f) << m;
168
+ if ((byte & 0x80) === 0) break;
169
+ m += 7;
170
+ }
171
+ return { value: value, next: pos };
172
+ }
173
+
174
+ function parseQpackHeaderBlock(buf, dynamicTable, baseIndex) {
175
+ var pos = 0;
176
+ var ric = decodeQpackVarInt(buf, 8, pos); pos = ric.next;
177
+ var postBase = (buf[pos] & 0x80) !== 0;
178
+ var db = decodeQpackVarInt(buf, 7, pos); pos = db.next;
179
+ var bi = postBase ? ric.value + db.value : ric.value - db.value;
180
+
181
+ var headers = {};
182
+ while (pos < buf.length) {
183
+ var byte = buf[pos];
184
+
185
+ if ((byte & 0x80) === 0x80) {
186
+ // Indexed
187
+ var fromStatic = (byte & 0x40) !== 0;
188
+ var idx = decodeQpackVarInt(buf, 6, pos); pos = idx.next;
189
+ if (fromStatic && idx.value < QPACK_STATIC.length) {
190
+ headers[QPACK_STATIC[idx.value][0]] = QPACK_STATIC[idx.value][1];
191
+ } else if (!fromStatic && dynamicTable) {
192
+ var di = bi - 1 - idx.value;
193
+ if (di >= 0 && di < dynamicTable.length) {
194
+ headers[dynamicTable[di][0]] = dynamicTable[di][1];
195
+ }
196
+ }
197
+
198
+ } else if ((byte & 0xc0) === 0x40) {
199
+ // Literal with name ref
200
+ var fromStatic = (byte & 0x10) !== 0;
201
+ var nameIdx = decodeQpackVarInt(buf, 4, pos); pos = nameIdx.next;
202
+ var valH = (buf[pos] & 0x80) !== 0;
203
+ var valLen = decodeQpackVarInt(buf, 7, pos); pos = valLen.next;
204
+ var valBytes = buf.slice(pos, pos + valLen.value); pos += valLen.value;
205
+ var value = valH ? huffDecode(valBytes) : new TextDecoder().decode(valBytes);
206
+
207
+ var name = null;
208
+ if (fromStatic && nameIdx.value < QPACK_STATIC.length) {
209
+ name = QPACK_STATIC[nameIdx.value][0];
210
+ } else if (!fromStatic && dynamicTable) {
211
+ var di = bi - 1 - nameIdx.value;
212
+ if (di >= 0 && di < dynamicTable.length) name = dynamicTable[di][0];
213
+ }
214
+ if (name) headers[name] = value;
215
+
216
+ } else if ((byte & 0xe0) === 0x20) {
217
+ // Literal with literal name
218
+ var nameH = (byte & 0x08) !== 0;
219
+ var nameLen = decodeQpackVarInt(buf, 3, pos); pos = nameLen.next;
220
+ var nameBytes = buf.slice(pos, pos + nameLen.value); pos += nameLen.value;
221
+ var name = nameH ? huffDecode(nameBytes) : new TextDecoder().decode(nameBytes);
222
+ var valH = (buf[pos] & 0x80) !== 0;
223
+ var valLen = decodeQpackVarInt(buf, 7, pos); pos = valLen.next;
224
+ var valBytes = buf.slice(pos, pos + valLen.value); pos += valLen.value;
225
+ var value = valH ? huffDecode(valBytes) : new TextDecoder().decode(valBytes);
226
+ headers[name] = value;
227
+
228
+ } else {
229
+ break;
230
+ }
231
+ }
232
+
233
+ return headers;
234
+ }
235
+
236
+ function encodeQpackInt(value, prefixBits) {
237
+ var max = (1 << prefixBits) - 1;
238
+ if (value < max) return [value];
239
+ var bytes = [max];
240
+ value -= max;
241
+ while (value >= 128) { bytes.push((value & 0x7f) | 0x80); value >>= 7; }
242
+ bytes.push(value);
243
+ return bytes;
244
+ }
245
+
246
+
247
+ // ---- Static table lookup maps (built once) ----
248
+
249
+ var _qpackFullMatch = {}; // "name\0value" → index
250
+ var _qpackNameMatch = {}; // "name" → first index
251
+
252
+ for (var _i = 0; _i < QPACK_STATIC.length; _i++) {
253
+ var _key = QPACK_STATIC[_i][0] + '\0' + QPACK_STATIC[_i][1];
254
+ if (!(_key in _qpackFullMatch)) _qpackFullMatch[_key] = _i;
255
+ if (!(QPACK_STATIC[_i][0] in _qpackNameMatch)) _qpackNameMatch[QPACK_STATIC[_i][0]] = _i;
256
+ }
257
+
258
+
259
+ function buildQpackHeaderBlock(headers) {
260
+ var out = [0x00, 0x00]; // QPACK prefix: RIC=0, delta base=0
261
+
262
+ for (var name in headers) {
263
+ var lname = name.toLowerCase();
264
+ var value = String(headers[name]);
265
+
266
+ // (1) Try full match: name + value in static table → indexed (1 byte)
267
+ var fullKey = lname + '\0' + value;
268
+ if (fullKey in _qpackFullMatch) {
269
+ var idx = _qpackFullMatch[fullKey];
270
+ // Indexed: 1 1 NNNNNN (T=1 static, 6-bit prefix)
271
+ var enc = encodeQpackInt(idx, 6);
272
+ out.push(0xc0 | enc[0]);
273
+ for (var i = 1; i < enc.length; i++) out.push(enc[i]);
274
+ continue;
275
+ }
276
+
277
+ // (2) Try name match: name in static table → literal with name ref
278
+ if (lname in _qpackNameMatch) {
279
+ var nameIdx = _qpackNameMatch[lname];
280
+ // Literal with name reference: 01 N T PPPP (N=0, T=1 static, 4-bit prefix)
281
+ var nameEnc = encodeQpackInt(nameIdx, 4);
282
+ out.push(0x50 | nameEnc[0]);
283
+ for (var i = 1; i < nameEnc.length; i++) out.push(nameEnc[i]);
284
+ // Value: H=0 (no huffman), 7-bit length prefix
285
+ var valBytes = new TextEncoder().encode(value);
286
+ var valLenEnc = encodeQpackInt(valBytes.length, 7);
287
+ out.push(valLenEnc[0]);
288
+ for (var i = 1; i < valLenEnc.length; i++) out.push(valLenEnc[i]);
289
+ for (var i = 0; i < valBytes.length; i++) out.push(valBytes[i]);
290
+ continue;
291
+ }
292
+
293
+ // (3) No match: literal with literal name (fallback)
294
+ var nameBytes = new TextEncoder().encode(lname);
295
+ var nameLenEnc = encodeQpackInt(nameBytes.length, 3);
296
+ out.push(0x20 | nameLenEnc[0]);
297
+ for (var i = 1; i < nameLenEnc.length; i++) out.push(nameLenEnc[i]);
298
+ for (var i = 0; i < nameBytes.length; i++) out.push(nameBytes[i]);
299
+ var valBytes = new TextEncoder().encode(value);
300
+ var valLenEnc = encodeQpackInt(valBytes.length, 7);
301
+ out.push(valLenEnc[0]);
302
+ for (var i = 1; i < valLenEnc.length; i++) out.push(valLenEnc[i]);
303
+ for (var i = 0; i < valBytes.length; i++) out.push(valBytes[i]);
304
+ }
305
+
306
+ return new Uint8Array(out);
307
+ }
308
+
309
+
310
+ // ============================================================
311
+ // H3 Frame build/parse
312
+ // ============================================================
313
+
314
+ function buildH3Frames(frames) {
315
+ var parts = [];
316
+ for (var i = 0; i < frames.length; i++) {
317
+ parts.push(writeVarInt(frames[i].frame_type));
318
+ parts.push(writeVarInt(frames[i].payload.length));
319
+ parts.push(frames[i].payload);
320
+ }
321
+ return concatUint8Arrays(parts);
322
+ }
323
+
324
+ function extractH3Frames(chunks, fromOffset) {
325
+ if (!chunks || chunks.length === 0) return { frames: [], new_from_offset: fromOffset };
326
+
327
+ var buffers = [];
328
+ var acc = 0;
329
+ for (var i = 0; i < chunks.length; i++) {
330
+ var nextAcc = acc + chunks[i].length;
331
+ if (fromOffset < nextAcc) {
332
+ buffers.push(chunks[i].slice(fromOffset - acc));
333
+ for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
334
+ break;
335
+ }
336
+ acc = nextAcc;
337
+ }
338
+
339
+ if (buffers.length === 0) return { frames: [], new_from_offset: fromOffset };
340
+
341
+ var combined = concatUint8Arrays(buffers);
342
+ var offset = 0, frames = [];
343
+
344
+ while (offset < combined.length) {
345
+ var start = offset;
346
+ var ft = readVarInt(combined, offset);
347
+ if (!ft) break;
348
+ offset += ft.byteLength;
349
+ var len = readVarInt(combined, offset);
350
+ if (!len) { offset = start; break; }
351
+ offset += len.byteLength;
352
+ if (offset + len.value > combined.length) { offset = start; break; }
353
+ frames.push({ frame_type: ft.value, payload: combined.slice(offset, offset + len.value) });
354
+ offset += len.value;
355
+ }
356
+
357
+ return { frames: frames, new_from_offset: fromOffset + offset };
358
+ }
359
+
360
+
361
+ // ============================================================
362
+ // H3 Settings
363
+ // ============================================================
364
+
365
+ var H3_SETTINGS_MAP = {
366
+ SETTINGS_QPACK_MAX_TABLE_CAPACITY: 0x01,
367
+ SETTINGS_MAX_FIELD_SECTION_SIZE: 0x06,
368
+ SETTINGS_ENABLE_CONNECT_PROTOCOL: 0x08,
369
+ SETTINGS_H3_DATAGRAM: 0x33,
370
+ SETTINGS_ENABLE_WEBTRANSPORT: 0x2b603742,
371
+ SETTINGS_WT_MAX_SESSIONS: 0x14e9cd29
372
+ };
373
+
374
+ var H3_ID_TO_NAME = {};
375
+ for (var k in H3_SETTINGS_MAP) H3_ID_TO_NAME[H3_SETTINGS_MAP[k]] = k;
376
+
377
+ function buildSettingsPayload(settings) {
378
+ var payload = [];
379
+ for (var name in settings) {
380
+ var id = H3_SETTINGS_MAP[name];
381
+ if (id === undefined) continue;
382
+ payload.push.apply(payload, Array.from(writeVarInt(id)));
383
+ payload.push.apply(payload, Array.from(writeVarInt(settings[name])));
384
+ }
385
+ return new Uint8Array(payload);
386
+ }
387
+
388
+ function parseSettingsPayload(buf) {
389
+ var out = {}, offset = 0;
390
+ while (offset < buf.length) {
391
+ var id = readVarInt(buf, offset); if (!id) break; offset += id.byteLength;
392
+ var val = readVarInt(buf, offset); if (!val) break; offset += val.byteLength;
393
+ var name = H3_ID_TO_NAME[id.value] || ('UNKNOWN_0x' + id.value.toString(16));
394
+ out[name] = val.value;
395
+ }
396
+ return out;
397
+ }
398
+
399
+
400
+ // ============================================================
401
+ // QPACK Encoder Instructions (from peer's encoder stream)
402
+ // ============================================================
403
+
404
+ function parseQpackEncoderInstructions(chunks, fromOffset) {
405
+ if (!chunks || chunks.length === 0) return { instructions: [], new_from_offset: fromOffset };
406
+
407
+ var buffers = [];
408
+ var acc = 0;
409
+ for (var i = 0; i < chunks.length; i++) {
410
+ var nextAcc = acc + chunks[i].length;
411
+ if (fromOffset < nextAcc) {
412
+ buffers.push(chunks[i].slice(fromOffset - acc));
413
+ for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
414
+ break;
415
+ }
416
+ acc = nextAcc;
417
+ }
418
+ if (buffers.length === 0) return { instructions: [], new_from_offset: fromOffset };
419
+
420
+ var combined = concatUint8Arrays(buffers);
421
+ var pos = 0;
422
+ var instructions = [];
423
+
424
+ function safeVarInt(prefixBits) {
425
+ if (pos >= combined.length) return null;
426
+ var max = (1 << prefixBits) - 1;
427
+ var value = combined[pos] & max;
428
+ var startPos = pos;
429
+ pos++;
430
+ if (value < max) return value;
431
+ var m = 0;
432
+ while (pos < combined.length) {
433
+ var b = combined[pos++];
434
+ value += (b & 0x7f) << m;
435
+ if ((b & 0x80) === 0) return value;
436
+ m += 7;
437
+ }
438
+ pos = startPos;
439
+ return null;
440
+ }
441
+
442
+ while (pos < combined.length) {
443
+ var startPos = pos;
444
+ var byte = combined[pos];
445
+
446
+ if ((byte & 0x80) === 0x80) {
447
+ // Insert with name reference
448
+ var fromStatic = (byte & 0x40) !== 0;
449
+ var nameIdx = safeVarInt(6); if (nameIdx === null) { pos = startPos; break; }
450
+ var valH = (combined[pos] & 0x80) !== 0;
451
+ var valLen = safeVarInt(7); if (valLen === null) { pos = startPos; break; }
452
+ if (pos + valLen > combined.length) { pos = startPos; break; }
453
+ var valBytes = combined.slice(pos, pos + valLen); pos += valLen;
454
+ var value = valH ? huffDecode(valBytes) : new TextDecoder().decode(valBytes);
455
+ instructions.push({ type: 'insert_name_ref', from_static: fromStatic, name_index: nameIdx, value: value });
456
+
457
+ } else if ((byte & 0xc0) === 0x40) {
458
+ // Insert without name reference
459
+ var nameH = (byte & 0x20) !== 0;
460
+ var nameLen = safeVarInt(5); if (nameLen === null) { pos = startPos; break; }
461
+ if (pos + nameLen > combined.length) { pos = startPos; break; }
462
+ var nameBytes = combined.slice(pos, pos + nameLen); pos += nameLen;
463
+ var name = nameH ? huffDecode(nameBytes) : new TextDecoder().decode(nameBytes);
464
+ var valH = (combined[pos] & 0x80) !== 0;
465
+ var valLen = safeVarInt(7); if (valLen === null) { pos = startPos; break; }
466
+ if (pos + valLen > combined.length) { pos = startPos; break; }
467
+ var valBytes = combined.slice(pos, pos + valLen); pos += valLen;
468
+ var value = valH ? huffDecode(valBytes) : new TextDecoder().decode(valBytes);
469
+ instructions.push({ type: 'insert_literal', name: name, value: value });
470
+
471
+ } else if ((byte & 0xe0) === 0x20) {
472
+ // Set dynamic table capacity
473
+ var capacity = safeVarInt(5); if (capacity === null) { pos = startPos; break; }
474
+ instructions.push({ type: 'set_capacity', capacity: capacity });
475
+
476
+ } else {
477
+ break;
478
+ }
479
+ }
480
+
481
+ return { instructions: instructions, new_from_offset: fromOffset + pos };
482
+ }
483
+
484
+
485
+ // ============================================================
486
+ // H3Connection — reactive pattern
487
+ // ============================================================
488
+ //
489
+ // Events:
490
+ // 'http_headers' (stream_id, headers) — new HTTP request headers
491
+ // 'http_body' (stream_id, data) — request body chunk
492
+ //
493
+
494
+ function H3Connection(options) {
495
+ if (!(this instanceof H3Connection)) return new H3Connection(options);
496
+ options = options || {};
497
+
498
+ var ev = Emitter();
499
+ var quic = options.quicConnection;
500
+
501
+
502
+ // ---- Context ----
503
+
504
+ var context = {
505
+ isServer: options.isServer !== false,
506
+
507
+ // Local settings (what we advertise)
508
+ local_max_header_size: 65536,
509
+ local_qpack_max_table_capacity: 0,
510
+ local_datagram_support: !!options.enableWebTransport,
511
+ local_webtransport: !!options.enableWebTransport,
512
+
513
+ // Remote settings (from peer's SETTINGS frame)
514
+ remote_max_header_size: 0,
515
+ remote_qpack_max_table_capacity: 0,
516
+ remote_datagram_support: null,
517
+
518
+ // QPACK dynamic table (peer's encoder inserts into this)
519
+ remote_qpack_dynamic_table: [],
520
+ remote_qpack_table_base_index: 0,
521
+ remote_qpack_table_capacity: 0,
522
+
523
+ // Stream tracking
524
+ streams: {}, // stream_id → { chunks, from_offset, type }
525
+
526
+ // Control stream state
527
+ control_streams_sent: false,
528
+ remote_settings_received: false,
529
+ };
530
+
531
+
532
+ // ---- set_context ----
533
+
534
+ function set_context(updates) {
535
+ if (!updates || typeof updates !== 'object') return;
536
+
537
+ var changed = {};
538
+
539
+ if ('remote_max_header_size' in updates && updates.remote_max_header_size !== context.remote_max_header_size) {
540
+ context.remote_max_header_size = updates.remote_max_header_size;
541
+ changed.remote_max_header_size = true;
542
+ }
543
+
544
+ if ('remote_qpack_max_table_capacity' in updates && updates.remote_qpack_max_table_capacity !== context.remote_qpack_max_table_capacity) {
545
+ context.remote_qpack_max_table_capacity = updates.remote_qpack_max_table_capacity;
546
+ changed.remote_qpack_max_table_capacity = true;
547
+ }
548
+
549
+ if ('remote_datagram_support' in updates) {
550
+ context.remote_datagram_support = !!updates.remote_datagram_support;
551
+ changed.remote_datagram_support = true;
552
+ }
553
+
554
+ if ('remote_qpack_table_capacity' in updates) {
555
+ context.remote_qpack_table_capacity = updates.remote_qpack_table_capacity;
556
+ changed.remote_qpack_table_capacity = true;
557
+ }
558
+
559
+ if ('remote_settings_received' in updates && !context.remote_settings_received) {
560
+ context.remote_settings_received = true;
561
+ changed.remote_settings_received = true;
562
+ }
563
+
564
+ if ('add_qpack_entries' in updates) {
565
+ var entries = updates.add_qpack_entries;
566
+ for (var i = 0; i < entries.length; i++) {
567
+ context.remote_qpack_dynamic_table.unshift(entries[i]);
568
+ context.remote_qpack_table_base_index++;
569
+ }
570
+ changed.qpack_table = true;
571
+ }
572
+
573
+ // ---- Reactive ----
574
+
575
+ if (changed.remote_qpack_table_capacity) {
576
+ evictQpackTable();
577
+ }
578
+
579
+ if (changed.remote_settings_received) {
580
+ // Could trigger QPACK table capacity update etc.
581
+ }
582
+ }
583
+
584
+
585
+ function evictQpackTable() {
586
+ var entries = context.remote_qpack_dynamic_table;
587
+ var capacity = context.remote_qpack_table_capacity;
588
+ var totalSize = 0;
589
+ for (var i = 0; i < entries.length; i++) {
590
+ totalSize += entries[i][0].length + entries[i][1].length + 32;
591
+ }
592
+ while (totalSize > capacity && entries.length > 0) {
593
+ var removed = entries.pop();
594
+ totalSize -= removed[0].length + removed[1].length + 32;
595
+ }
596
+ }
597
+
598
+
599
+ // ---- Send initial control + QPACK streams ----
600
+
601
+ function sendControlStreams() {
602
+ if (context.control_streams_sent) return;
603
+ context.control_streams_sent = true;
604
+
605
+ var settingsObj = {
606
+ SETTINGS_MAX_FIELD_SECTION_SIZE: context.local_max_header_size
607
+ };
608
+ if (context.local_qpack_max_table_capacity > 0) {
609
+ settingsObj.SETTINGS_QPACK_MAX_TABLE_CAPACITY = context.local_qpack_max_table_capacity;
610
+ }
611
+ if (context.local_datagram_support) {
612
+ settingsObj.SETTINGS_H3_DATAGRAM = 1;
613
+ settingsObj.SETTINGS_ENABLE_CONNECT_PROTOCOL = 1;
614
+ }
615
+ if (context.local_webtransport) {
616
+ settingsObj.SETTINGS_ENABLE_WEBTRANSPORT = 1;
617
+ }
618
+ var settings = buildSettingsPayload(settingsObj);
619
+
620
+ var controlFrame = buildH3Frames([{ frame_type: 0x04, payload: settings }]);
621
+
622
+ // Uni stream IDs: server=3,7,11 client=2,6,10
623
+ var ctrlId = context.isServer ? 3 : 2;
624
+ var encId = context.isServer ? 7 : 6;
625
+ var decId = context.isServer ? 11 : 10;
626
+
627
+ console.log('[h3] sendControlStreams ctrl=' + ctrlId + ' enc=' + encId + ' dec=' + decId);
628
+ console.log('[h3] SETTINGS hex: ' + Array.from(controlFrame).map(function(b){ return b.toString(16).padStart(2,'0'); }).join(' '));
629
+
630
+ // Send type byte + SETTINGS together as one chunk (some implementations need this)
631
+ var ctrlData = new Uint8Array(1 + controlFrame.byteLength);
632
+ ctrlData[0] = 0x00; // control stream type
633
+ ctrlData.set(controlFrame, 1);
634
+ quic.sendStream(ctrlId, ctrlData, false);
635
+ quic.sendStream(encId, new Uint8Array([0x02]), false); // QPACK encoder
636
+ quic.sendStream(decId, new Uint8Array([0x03]), false); // QPACK decoder
637
+ }
638
+
639
+
640
+ // ---- Incoming stream data ----
641
+
642
+ var _claimedStreams = new Set(); // streams owned by WebTransport (skip H3 processing)
643
+ var _wtSessions = new Set(); // active WT session stream IDs
644
+ var _wtStreams = {}; // streamId → { sessionId, isBidi }
645
+
646
+ quic.on('stream', function (streamId, data, fin) {
647
+ var sid = Number(streamId);
648
+
649
+ // Route data for already-detected WT streams
650
+ if (sid in _wtStreams) {
651
+ var ws = _wtStreams[sid];
652
+ ev.emit('wt_data', ws.sessionId, sid, data, fin);
653
+ if (fin) delete _wtStreams[sid];
654
+ return;
655
+ }
656
+
657
+ if (_claimedStreams.has(sid)) return; // owned by WebTransport client
658
+ console.log('[h3] stream ' + streamId + ' data=' + data.byteLength + ' fin=' + fin);
659
+ if (!(streamId in context.streams)) {
660
+ context.streams[streamId] = { chunks: [], from_offset: 0, type: null, fin_received: false };
661
+ }
662
+
663
+ var s = context.streams[streamId];
664
+ s.chunks.push(data);
665
+ if (fin) s.fin_received = true;
666
+
667
+ // Identify stream type
668
+ if (s.type === null) {
669
+ var isUni = (Number(streamId) & 0x2) !== 0;
670
+
671
+ // ---- WT stream detection ----
672
+ if (context.local_webtransport && _wtSessions.size > 0) {
673
+ var allData = concatUint8Arrays(s.chunks);
674
+ console.log('[h3] WT check: stream=' + sid + ' firstByte=0x' + (allData[0] || 0).toString(16) + ' len=' + allData.byteLength + ' sessions=' + Array.from(_wtSessions));
675
+ if (allData.byteLength >= 2 && allData[0] === 0x40) {
676
+ var typeResult = readVarInt(allData, 0);
677
+ if (typeResult) {
678
+ var expectedType = isUni ? 0x54 : 0x41;
679
+ if (typeResult.value === expectedType) {
680
+ var afterType = allData.slice(typeResult.byteLength);
681
+ var sidResult = readVarInt(afterType, 0);
682
+ if (sidResult && _wtSessions.has(sidResult.value)) {
683
+ // WT stream detected
684
+ var sessionId = sidResult.value;
685
+ var prefixLen = typeResult.byteLength + sidResult.byteLength;
686
+ var remaining = allData.byteLength > prefixLen ? allData.slice(prefixLen) : new Uint8Array(0);
687
+
688
+ _claimedStreams.add(sid);
689
+ _wtStreams[sid] = { sessionId: sessionId, isBidi: !isUni };
690
+ delete context.streams[streamId];
691
+
692
+ console.log('[h3] WT stream detected: stream=' + sid + ' session=' + sessionId + ' bidi=' + !isUni + ' dataLen=' + remaining.byteLength);
693
+ ev.emit('wt_stream', sessionId, sid, remaining, fin, !isUni);
694
+ if (fin) delete _wtStreams[sid];
695
+ return;
696
+ }
697
+ }
698
+ }
699
+ }
700
+ }
701
+
702
+ if (isUni) {
703
+ if (s.chunks[0].length > 0) {
704
+ var first = s.chunks[0][0];
705
+ if (first === 0x00) { s.type = 'control'; s.from_offset = 1; }
706
+ else if (first === 0x02) { s.type = 'qpack_enc'; s.from_offset = 1; }
707
+ else if (first === 0x03) { s.type = 'qpack_dec'; s.from_offset = 1; }
708
+ else { s.type = 'unknown'; }
709
+ }
710
+ } else {
711
+ s.type = 'bidi';
712
+ }
713
+ }
714
+
715
+ // Process by type
716
+ if (s.type === 'control') {
717
+ processControlStream(s);
718
+
719
+ } else if (s.type === 'qpack_enc') {
720
+ processQpackEncoderStream(s);
721
+
722
+ } else if (s.type === 'bidi') {
723
+ processBidiStream(streamId, s);
724
+ }
725
+ });
726
+
727
+
728
+ quic.on('connect', function () {
729
+ sendControlStreams();
730
+ });
731
+
732
+ // If QUIC already connected (client creates H3 after connect), send now
733
+ if (quic.state === 'connected') {
734
+ sendControlStreams();
735
+ }
736
+
737
+
738
+ // ---- Stream processors ----
739
+
740
+ function processControlStream(s) {
741
+ var ext = extractH3Frames(s.chunks, s.from_offset);
742
+ console.log('[h3] control: extracted ' + ext.frames.length + ' H3 frames');
743
+ if (ext.frames.length === 0) return;
744
+ s.from_offset = ext.new_from_offset;
745
+
746
+ for (var i = 0; i < ext.frames.length; i++) {
747
+ if (ext.frames[i].frame_type === 0x04) {
748
+ var settings = parseSettingsPayload(ext.frames[i].payload);
749
+ var updates = { remote_settings_received: true };
750
+
751
+ if ('SETTINGS_QPACK_MAX_TABLE_CAPACITY' in settings) {
752
+ updates.remote_qpack_max_table_capacity = settings['SETTINGS_QPACK_MAX_TABLE_CAPACITY'];
753
+ }
754
+ if ('SETTINGS_MAX_FIELD_SECTION_SIZE' in settings) {
755
+ updates.remote_max_header_size = settings['SETTINGS_MAX_FIELD_SECTION_SIZE'];
756
+ }
757
+ if ('SETTINGS_H3_DATAGRAM' in settings) {
758
+ updates.remote_datagram_support = settings['SETTINGS_H3_DATAGRAM'] > 0;
759
+ }
760
+
761
+ set_context(updates);
762
+
763
+ } else if (ext.frames[i].frame_type === 0x07) {
764
+ // GOAWAY — TODO
765
+ }
766
+ }
767
+ }
768
+
769
+ function processQpackEncoderStream(s) {
770
+ var ext = parseQpackEncoderInstructions(s.chunks, s.from_offset);
771
+ if (ext.instructions.length === 0) return;
772
+ s.from_offset = ext.new_from_offset;
773
+
774
+ var inserts = [];
775
+ for (var i = 0; i < ext.instructions.length; i++) {
776
+ var inst = ext.instructions[i];
777
+ if (inst.type === 'set_capacity') {
778
+ set_context({ remote_qpack_table_capacity: inst.capacity });
779
+
780
+ } else if (inst.type === 'insert_name_ref') {
781
+ var name = null;
782
+ if (inst.from_static && inst.name_index < QPACK_STATIC.length) {
783
+ name = QPACK_STATIC[inst.name_index][0];
784
+ } else if (!inst.from_static) {
785
+ var di = context.remote_qpack_table_base_index - 1 - inst.name_index;
786
+ if (di >= 0 && di < context.remote_qpack_dynamic_table.length) {
787
+ name = context.remote_qpack_dynamic_table[di][0];
788
+ }
789
+ }
790
+ if (name) inserts.push([name, inst.value]);
791
+
792
+ } else if (inst.type === 'insert_literal') {
793
+ inserts.push([inst.name, inst.value]);
794
+ }
795
+ }
796
+
797
+ if (inserts.length > 0) {
798
+ set_context({ add_qpack_entries: inserts });
799
+ }
800
+ }
801
+
802
+ function processBidiStream(streamId, s) {
803
+ var ext = extractH3Frames(s.chunks, s.from_offset);
804
+ console.log('[h3] bidi stream ' + streamId + ': extracted ' + ext.frames.length + ' H3 frames, types=' + ext.frames.map(function(f){ return '0x' + f.frame_type.toString(16); }).join(','));
805
+
806
+ if (ext.frames.length > 0) {
807
+ s.from_offset = ext.new_from_offset;
808
+
809
+ for (var i = 0; i < ext.frames.length; i++) {
810
+ if (ext.frames[i].frame_type === 1) {
811
+ // HEADERS
812
+ var headers = parseQpackHeaderBlock(
813
+ ext.frames[i].payload,
814
+ context.remote_qpack_dynamic_table,
815
+ context.remote_qpack_table_base_index
816
+ );
817
+ ev.emit('http_headers', Number(streamId), headers);
818
+
819
+ } else if (ext.frames[i].frame_type === 0) {
820
+ // DATA
821
+ ev.emit('http_body', Number(streamId), ext.frames[i].payload);
822
+ }
823
+ }
824
+ }
825
+
826
+ // FIN received → stream complete (emitted only once)
827
+ if (s.fin_received && !s.http_end_emitted) {
828
+ s.http_end_emitted = true;
829
+ ev.emit('http_end', Number(streamId));
830
+ }
831
+ }
832
+
833
+
834
+ // ---- Send methods ----
835
+
836
+ function sendHeaders(streamId, headers, fin) {
837
+ var payload = buildQpackHeaderBlock(headers);
838
+ var frame = buildH3Frames([{ frame_type: 1, payload: payload }]);
839
+ console.log('[h3] sendHeaders stream=' + streamId + ' len=' + frame.byteLength + ' fin=' + !!fin);
840
+ quic.sendStream(streamId, frame, !!fin);
841
+ }
842
+
843
+ function sendBody(streamId, data, fin) {
844
+ console.log('[h3] sendBody stream=' + streamId + ' data=' + (data ? data.byteLength : 'null') + ' fin=' + fin);
845
+ if (data === null || data === undefined) {
846
+ // Just set FIN on the stream (no additional H3 frame needed for empty body)
847
+ quic.sendStream(streamId, new Uint8Array(0), true);
848
+ return;
849
+ }
850
+ if (typeof data === 'string') data = new TextEncoder().encode(data);
851
+ var frame = buildH3Frames([{ frame_type: 0, payload: data }]);
852
+ quic.sendStream(streamId, frame, !!fin);
853
+ }
854
+
855
+
856
+ // ---- Public API ----
857
+
858
+ var api = {
859
+ context: context,
860
+
861
+ on: function (name, fn) { ev.on(name, fn); },
862
+ set_context: set_context,
863
+
864
+ sendHeaders: sendHeaders,
865
+ sendBody: sendBody,
866
+ sendControlStreams: sendControlStreams,
867
+ claimStream: function (streamId) { _claimedStreams.add(Number(streamId)); },
868
+
869
+ // WebTransport server-side API
870
+ registerWebTransportSession: function (sessionStreamId) {
871
+ _wtSessions.add(Number(sessionStreamId));
872
+ },
873
+
874
+ createWebTransportStream: function (sessionId, isBidi) {
875
+ // Allocate next stream ID (server-initiated)
876
+ // Server bidi: 1, 5, 9, 13... (type 0x1)
877
+ // Server uni: 3, 7, 11, 15... (type 0x3) — but 3,7,11 used by H3
878
+ var streamId;
879
+ if (isBidi) {
880
+ if (!context._nextServerBidiId) context._nextServerBidiId = 1;
881
+ streamId = context._nextServerBidiId;
882
+ context._nextServerBidiId += 4;
883
+ } else {
884
+ if (!context._nextServerUniId) context._nextServerUniId = 15;
885
+ streamId = context._nextServerUniId;
886
+ context._nextServerUniId += 4;
887
+ }
888
+
889
+ // Send WT prefix: VarInt(type) + VarInt(session_id)
890
+ var typePrefix = writeVarInt(isBidi ? 0x41 : 0x54);
891
+ var sessionPrefix = writeVarInt(sessionId);
892
+ var header = new Uint8Array(typePrefix.length + sessionPrefix.length);
893
+ header.set(typePrefix, 0);
894
+ header.set(sessionPrefix, typePrefix.length);
895
+ quic.sendStream(streamId, header, false);
896
+
897
+ _claimedStreams.add(streamId);
898
+ _wtStreams[streamId] = { sessionId: sessionId, isBidi: isBidi };
899
+
900
+ return streamId;
901
+ }
902
+ };
903
+
904
+ for (var k in api) {
905
+ if (Object.prototype.hasOwnProperty.call(api, k)) this[k] = api[k];
906
+ }
907
+ return this;
908
+ }
909
+
910
+
911
+ export { H3Connection };