quico 0.1.1 → 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/README.md +308 -70
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +46 -2631
- package/package.json +69 -14
- package/src/agent.js +275 -0
- package/src/client.js +439 -0
- package/src/crypto.js +519 -0
- package/src/h3.js +911 -0
- package/src/h3_client.js +317 -0
- package/src/h3_server.js +523 -0
- package/src/quic_connection.js +1238 -0
- package/src/server.js +346 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +211 -0
- package/src/transport.js +651 -0
- package/src/utils.js +290 -0
- package/src/webtransport.js +708 -0
- package/CHANGELOG.md +0 -25
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -29
- package/libs/crypto.js +0 -2107
- package/libs/h3.js +0 -1214
- package/libs/utils.js +0 -404
- package/quico.svg +0 -1
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 };
|