quico 0.4.0 → 0.8.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 +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/h3.js
CHANGED
|
@@ -12,6 +12,51 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import {
|
|
16
|
+
createQpackTable,
|
|
17
|
+
createQpackHistory,
|
|
18
|
+
encodeHeaderBlock as qpackEncodeHeaderBlock,
|
|
19
|
+
encodeInsert as qpackEncodeInsert,
|
|
20
|
+
encodeSetCapacity as qpackEncodeSetCapacity,
|
|
21
|
+
parseDecoderInstructions as qpackParseDecoderInstructions,
|
|
22
|
+
encodeInsertCountIncrement as qpackEncodeAck,
|
|
23
|
+
encodeStreamCancellation as qpackEncodeStreamCancel,
|
|
24
|
+
decodeRequiredInsertCount,
|
|
25
|
+
encodeSectionAck as qpackEncodeSectionAck,
|
|
26
|
+
entrySize as qpackEntrySize
|
|
27
|
+
} from './qpack.js';
|
|
28
|
+
import { validateFieldSection, parseContentLength, parsePriority, formatPriority } from './http_common.js';
|
|
29
|
+
|
|
30
|
+
// ============================================================
|
|
31
|
+
// QPACK dynamic table (RFC 9204): ON.
|
|
32
|
+
//
|
|
33
|
+
// A kill switch rather than a constant, because the dynamic table is the one
|
|
34
|
+
// part of HTTP/3 whose failure is invisible to a quico-to-quico test: both
|
|
35
|
+
// ends of such a test share this file's encoder and decoder, so a mistake
|
|
36
|
+
// made the same way on both sides passes. That is exactly what happened
|
|
37
|
+
// once — the Required Insert Count was sent raw and read raw, the two agreed,
|
|
38
|
+
// and every real browser failed from its fourth request on.
|
|
39
|
+
//
|
|
40
|
+
// Verified since against independent implementations in both directions:
|
|
41
|
+
// aioquic/ls-qpack and Chromium. Set to false to fall back to static-table
|
|
42
|
+
// and Huffman compression only, with no other change.
|
|
43
|
+
// ============================================================
|
|
44
|
+
var QPACK_DYNAMIC_TABLE = true;
|
|
45
|
+
|
|
46
|
+
// The dynamic table's size, both ways: what we let the peer's encoder use, and
|
|
47
|
+
// the most our encoder uses of what the peer allows. 4096 is what browsers
|
|
48
|
+
// advertise, and it is enough: the fields that repeat and are not in the
|
|
49
|
+
// static table — user-agent, accept, sec-ch-ua — total a few hundred bytes.
|
|
50
|
+
var QPACK_TABLE_BYTES = 4096;
|
|
51
|
+
|
|
52
|
+
// Streams that may wait for the peer's encoder stream at once (SETTINGS_
|
|
53
|
+
// QPACK_BLOCKED_STREAMS, RFC 9204 §2.1.2): a field section may then
|
|
54
|
+
// reference an entry the encoder has only just inserted — its insertion and
|
|
55
|
+
// the section travel on different streams, and either can arrive first —
|
|
56
|
+
// rather than wait a round trip for our acknowledgement, or not compress the
|
|
57
|
+
// field at all. 0 was advertised. Chromium advertises 100.
|
|
58
|
+
var QPACK_BLOCKED_STREAMS = 100;
|
|
59
|
+
|
|
15
60
|
import {
|
|
16
61
|
DEBUG,
|
|
17
62
|
Emitter,
|
|
@@ -20,301 +65,198 @@ import {
|
|
|
20
65
|
readVarInt,
|
|
21
66
|
wt_stream_header
|
|
22
67
|
} from './utils.js';
|
|
68
|
+
import {
|
|
69
|
+
QPACK_STATIC,
|
|
70
|
+
readPrefixedInt,
|
|
71
|
+
decodeStringLiteral
|
|
72
|
+
} from './hpack_primitives.js';
|
|
23
73
|
|
|
24
74
|
|
|
25
75
|
// ============================================================
|
|
26
|
-
// QPACK
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
[':status','404'], [':status','503'], ['accept','*/*'],
|
|
39
|
-
['accept','application/dns-message'], ['accept-encoding','gzip, deflate, br'],
|
|
40
|
-
['accept-ranges','bytes'], ['access-control-allow-headers','cache-control'],
|
|
41
|
-
['access-control-allow-headers','content-type'],
|
|
42
|
-
['access-control-allow-origin','*'], ['cache-control','max-age=0'],
|
|
43
|
-
['cache-control','max-age=2592000'], ['cache-control','max-age=604800'],
|
|
44
|
-
['cache-control','no-cache'], ['cache-control','no-store'],
|
|
45
|
-
['cache-control','public, max-age=31536000'], ['content-encoding','br'],
|
|
46
|
-
['content-encoding','gzip'], ['content-type','application/dns-message'],
|
|
47
|
-
['content-type','application/javascript'], ['content-type','application/json'],
|
|
48
|
-
['content-type','application/x-www-form-urlencoded'],
|
|
49
|
-
['content-type','image/gif'], ['content-type','image/jpeg'],
|
|
50
|
-
['content-type','image/png'], ['content-type','text/css'],
|
|
51
|
-
['content-type','text/html; charset=utf-8'], ['content-type','text/plain'],
|
|
52
|
-
['content-type','text/plain;charset=utf-8'], ['range','bytes=0-'],
|
|
53
|
-
['strict-transport-security','max-age=31536000'],
|
|
54
|
-
['strict-transport-security','max-age=31536000; includesubdomains'],
|
|
55
|
-
['strict-transport-security','max-age=31536000; includesubdomains; preload'],
|
|
56
|
-
['vary','accept-encoding'], ['vary','origin'],
|
|
57
|
-
['x-content-type-options','nosniff'], ['x-xss-protection','1; mode=block'],
|
|
58
|
-
[':status','100'], [':status','204'], [':status','206'], [':status','302'],
|
|
59
|
-
[':status','400'], [':status','403'], [':status','421'], [':status','425'],
|
|
60
|
-
[':status','500'], ['accept-language',''],
|
|
61
|
-
['access-control-allow-credentials','FALSE'],
|
|
62
|
-
['access-control-allow-credentials','TRUE'],
|
|
63
|
-
['access-control-allow-headers','*'],
|
|
64
|
-
['access-control-allow-methods','get'],
|
|
65
|
-
['access-control-allow-methods','get, post, options'],
|
|
66
|
-
['access-control-allow-methods','options'],
|
|
67
|
-
['access-control-expose-headers','content-length'],
|
|
68
|
-
['access-control-request-headers','content-type'],
|
|
69
|
-
['access-control-request-method','get'],
|
|
70
|
-
['access-control-request-method','post'], ['alt-svc','clear'],
|
|
71
|
-
['authorization',''],
|
|
72
|
-
['content-security-policy',"script-src 'none'; object-src 'none'; base-uri 'none'"],
|
|
73
|
-
['early-data','1'], ['expect-ct',''], ['forwarded',''], ['if-range',''],
|
|
74
|
-
['origin',''], ['purpose','prefetch'], ['server',''],
|
|
75
|
-
['timing-allow-origin','*'], ['upgrade-insecure-requests','1'],
|
|
76
|
-
['user-agent',''], ['x-forwarded-for',''],
|
|
77
|
-
['x-frame-options','deny'], ['x-frame-options','sameorigin']
|
|
78
|
-
];
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// ============================================================
|
|
82
|
-
// Huffman (RFC 7541 Appendix B)
|
|
76
|
+
// QPACK header blocks (RFC 9204)
|
|
77
|
+
//
|
|
78
|
+
// The wire primitives — Huffman, prefixed integers, string literals — now live
|
|
79
|
+
// in hpack_primitives.js, because RFC 9204 defines them by reference to
|
|
80
|
+
// RFC 7541 and HTTP/2's HPACK needs byte-identical code. What stays here is
|
|
81
|
+
// what is genuinely QPACK: the RIC/base-index prefix, the encoder-stream
|
|
82
|
+
// instructions, and the static table's own contents.
|
|
83
|
+
//
|
|
84
|
+
// Names and values are turned into text with TextDecoder (UTF-8), which is
|
|
85
|
+
// what this file has always done. hpack_primitives exports bytesToLatin1 as a
|
|
86
|
+
// lossless alternative; switching to it is a deliberate behaviour change and
|
|
87
|
+
// deliberately NOT part of this move.
|
|
83
88
|
// ============================================================
|
|
84
89
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
0x16,0x17,0x18,0x0,0x1,0x2,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x5c,0xfb,
|
|
92
|
-
0x7ffc,0x20,0xffb,0x3fc,0x1ffa,0x21,0x5d,0x5e,0x5f,0x60,0x61,0x62,0x63,0x64,
|
|
93
|
-
0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xfc,
|
|
94
|
-
0x73,0xfd,0x1ffb,0x7fff0,0x1ffc,0x3ffc,0x22,0x7ffd,0x3,0x23,0x4,0x24,0x5,0x25,
|
|
95
|
-
0x26,0x27,0x6,0x74,0x75,0x28,0x29,0x2a,0x7,0x2b,0x76,0x2c,0x8,0x9,0x2d,0x77,
|
|
96
|
-
0x78,0x79,0x7a,0x7b,0x7ffe,0x7fc,0x3ffd,0x1ffd,0xffffffc,0xfffe6,0x3fffd2,
|
|
97
|
-
0xfffe7,0xfffe8,0x3fffd3,0x3fffd4,0x3fffd5,0x7fffd9,0x3fffd6,0x7fffda,0x7fffdb,
|
|
98
|
-
0x7fffdc,0x7fffdd,0x7fffde,0xffffeb,0x7fffdf,0xffffec,0xffffed,0x3fffd7,0x7fffe0,
|
|
99
|
-
0xffffee,0x7fffe1,0x7fffe2,0x7fffe3,0x7fffe4,0x1fffdc,0x3fffd8,0x7fffe5,0x3fffd9,
|
|
100
|
-
0x7fffe6,0x7fffe7,0xffffef,0x3fffda,0x1fffdd,0xfffe9,0x3fffdb,0x3fffdc,0x7fffe8,
|
|
101
|
-
0x7fffe9,0x1fffde,0x7fffea,0x3fffdd,0x3fffde,0xfffff0,0x1fffdf,0x3fffdf,0x7fffeb,
|
|
102
|
-
0x7fffec,0x1fffe0,0x1fffe1,0x3fffe0,0x1fffe2,0x7fffed,0x3fffe1,0x7fffee,0x7fffef,
|
|
103
|
-
0xfffea,0x3fffe2,0x3fffe3,0x3fffe4,0x7ffff0,0x3fffe5,0x3fffe6,0x7ffff1,0x3ffffe0,
|
|
104
|
-
0x3ffffe1,0xfffeb,0x7fff1,0x3fffe7,0x7ffff2,0x3fffe8,0x1ffffec,0x3ffffe2,0x3ffffe3,
|
|
105
|
-
0x3ffffe4,0x7ffffde,0x7ffffdf,0x3ffffe5,0xfffff1,0x1ffffed,0x7fff2,0x1fffe3,
|
|
106
|
-
0x3ffffe6,0x7ffffe0,0x7ffffe1,0x3ffffe7,0x7ffffe2,0xfffff2,0x1fffe4,0x1fffe5,
|
|
107
|
-
0x3ffffe8,0x3ffffe9,0xffffffd,0x7ffffe3,0x7ffffe4,0x7ffffe5,0xfffec,0xfffff3,
|
|
108
|
-
0xfffed,0x1fffe6,0x3fffe9,0x1fffe7,0x1fffe8,0x7ffff3,0x3fffea,0x3fffeb,0x1ffffee,
|
|
109
|
-
0x1ffffef,0xfffff4,0xfffff5,0x3ffffea,0x7ffff4,0x3ffffeb,0x7ffffe6,0x3ffffec,
|
|
110
|
-
0x3ffffed,0x7ffffe7,0x7ffffe8,0x7ffffe9,0x7ffffea,0x7ffffeb,0xffffffe,0x7ffffec,
|
|
111
|
-
0x7ffffed,0x7ffffee,0x7ffffef,0x7fffff0,0x3ffffee,0x3fffffff
|
|
112
|
-
]);
|
|
113
|
-
|
|
114
|
-
var HUFF_BITS = new Uint8Array([
|
|
115
|
-
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,
|
|
116
|
-
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,
|
|
117
|
-
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,
|
|
118
|
-
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,
|
|
119
|
-
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,
|
|
120
|
-
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,
|
|
121
|
-
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,
|
|
122
|
-
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,
|
|
123
|
-
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,
|
|
124
|
-
28,27,27,27,27,27,26,30
|
|
125
|
-
]);
|
|
126
|
-
|
|
127
|
-
var _huffRoot = (function () {
|
|
128
|
-
var root = {};
|
|
129
|
-
for (var i = 0; i < HUFF_CODES.length; i++) {
|
|
130
|
-
var code = HUFF_CODES[i], bits = HUFF_BITS[i], node = root;
|
|
131
|
-
for (var j = bits - 1; j >= 0; j--) {
|
|
132
|
-
var bit = (code >> j) & 1;
|
|
133
|
-
if (!node[bit]) node[bit] = {};
|
|
134
|
-
node = node[bit];
|
|
135
|
-
}
|
|
136
|
-
node.sym = i;
|
|
137
|
-
}
|
|
138
|
-
return root;
|
|
139
|
-
})();
|
|
140
|
-
|
|
141
|
-
function huffDecode(buf) {
|
|
142
|
-
var out = [], node = _huffRoot, nbits = 0, current = 0;
|
|
143
|
-
for (var i = 0; i < buf.length; i++) {
|
|
144
|
-
current = (current << 8) | buf[i];
|
|
145
|
-
nbits += 8;
|
|
146
|
-
while (nbits > 0) {
|
|
147
|
-
node = node[(current >> (nbits - 1)) & 1];
|
|
148
|
-
if (!node) throw new Error('Invalid Huffman');
|
|
149
|
-
nbits--;
|
|
150
|
-
if (node.sym !== undefined) { out.push(node.sym); node = _huffRoot; }
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
return new TextDecoder().decode(Uint8Array.from(out));
|
|
90
|
+
// One decoder for the module: decode() holds no state between calls (no
|
|
91
|
+
// `stream` option), and constructing one per name and per value cost more
|
|
92
|
+
// than the decoding itself.
|
|
93
|
+
var _utf8Decoder = new TextDecoder();
|
|
94
|
+
function _text(bytes) {
|
|
95
|
+
return _utf8Decoder.decode(bytes);
|
|
154
96
|
}
|
|
155
97
|
|
|
98
|
+
// Every decodeStringLiteral() call below passes strict = true, enabling the
|
|
99
|
+
// padding checks of RFC 7541 §5.2: padding over 7 bits, padding that is not the
|
|
100
|
+
// EOS prefix, and a literal EOS symbol are all rejected. Without it an all-ones
|
|
101
|
+
// run decodes to symbol 256, which then silently truncates to a zero byte and
|
|
102
|
+
// lands in a header value — a malformed block quietly becomes a valid-looking
|
|
103
|
+
// request. Every peer that matters (nghttp2, quiche, msquic, Chrome) pads
|
|
104
|
+
// correctly, and the QPACK interop tests check that a decoder rejects the ones
|
|
105
|
+
// that do not.
|
|
106
|
+
//
|
|
107
|
+
// Note what strict does NOT cover: a literal cut short by the end of the
|
|
108
|
+
// available bytes still returns null and the parser waits for more, because on
|
|
109
|
+
// a QUIC stream that is ordinary partial delivery, not corruption. Telling the
|
|
110
|
+
// two apart needs the stream's FIN — a block still incomplete at FIN is a real
|
|
111
|
+
// error, and that check belongs with the per-stream state, not here.
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
function parseQpackHeaderBlock(buf, dynamicTable, totalInserts, maxEntries, maxSectionSize) {
|
|
116
|
+
// RFC 9204 §4.5. `dynamicTable` is newest-first; `totalInserts` is how many
|
|
117
|
+
// entries the peer has ever inserted, so absolute index A (zero-based) sits
|
|
118
|
+
// at array position totalInserts - 1 - A — if it has not been evicted.
|
|
119
|
+
//
|
|
120
|
+
// This used to read the Required Insert Count raw, compute Base with the
|
|
121
|
+
// sign bit's meaning reversed, and index the array by the absolute index.
|
|
122
|
+
// None of it ran while our advertised table capacity was 0; the moment it
|
|
123
|
+
// was raised, every conforming encoder broke on its first dynamic reference.
|
|
124
|
+
var pos = 0;
|
|
125
|
+
var ricEnc = readPrefixedInt(buf, 8, pos); if (!ricEnc) throw new Error('QPACK: truncated field section prefix');
|
|
126
|
+
pos = ricEnc.next;
|
|
127
|
+
if (pos >= buf.length) throw new Error('QPACK: truncated field section prefix');
|
|
128
|
+
var sign = (buf[pos] & 0x80) !== 0;
|
|
129
|
+
var db = readPrefixedInt(buf, 7, pos); if (!db) throw new Error('QPACK: truncated field section prefix');
|
|
130
|
+
pos = db.next;
|
|
131
|
+
|
|
132
|
+
var ric = decodeRequiredInsertCount(ricEnc.value, maxEntries || 0, totalInserts || 0);
|
|
133
|
+
|
|
134
|
+
// A section past the inserts received waits before it is decoded (the
|
|
135
|
+
// caller, sectionBlocked): here, it is one that should not have come.
|
|
136
|
+
if (ric > (totalInserts || 0)) {
|
|
137
|
+
throw new Error('QPACK: section needs ' + ric + ' inserts, have ' + (totalInserts || 0));
|
|
138
|
+
}
|
|
156
139
|
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
140
|
+
// §4.5.1.2: sign 0 → Base = RIC + Delta; sign 1 → Base = RIC - Delta - 1.
|
|
141
|
+
var base = sign ? ric - db.value - 1 : ric + db.value;
|
|
142
|
+
if (base < 0) throw new Error('QPACK: negative Base');
|
|
160
143
|
|
|
161
|
-
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
while (pos < buf.length) {
|
|
172
|
-
var byte = buf[pos++];
|
|
173
|
-
value += (byte & 0x7f) << m;
|
|
174
|
-
if ((byte & 0x80) === 0) return { value: value, next: pos };
|
|
175
|
-
m += 7;
|
|
144
|
+
function entryAt(absolute) {
|
|
145
|
+
// Only entries below the declared Required Insert Count may be referenced.
|
|
146
|
+
if (absolute < 0 || absolute >= ric) {
|
|
147
|
+
throw new Error('QPACK: dynamic reference ' + absolute + ' outside Required Insert Count ' + ric);
|
|
148
|
+
}
|
|
149
|
+
var p = totalInserts - 1 - absolute;
|
|
150
|
+
if (!dynamicTable || p < 0 || p >= dynamicTable.length) {
|
|
151
|
+
throw new Error('QPACK: dynamic entry ' + absolute + ' unresolvable (evicted or never inserted)');
|
|
152
|
+
}
|
|
153
|
+
return dynamicTable[p];
|
|
176
154
|
}
|
|
177
|
-
return null; // truncated mid-integer
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function parseQpackHeaderBlock(buf, dynamicTable, baseIndex) {
|
|
181
|
-
var pos = 0;
|
|
182
|
-
var ric = readPrefixedInt(buf, 8, pos); if (!ric) return {}; pos = ric.next;
|
|
183
|
-
var postBase = (buf[pos] & 0x80) !== 0;
|
|
184
|
-
var db = readPrefixedInt(buf, 7, pos); if (!db) return {}; pos = db.next;
|
|
185
|
-
var bi = postBase ? ric.value + db.value : ric.value - db.value;
|
|
186
155
|
|
|
187
156
|
var headers = {};
|
|
157
|
+
// The field names in the order they were decoded.
|
|
158
|
+
//
|
|
159
|
+
// An object cannot carry it — pseudo-headers and regular fields end up in
|
|
160
|
+
// whatever order the engine chooses — and the order is the strongest single
|
|
161
|
+
// signal in a protocol fingerprint: Chrome sends m,a,s,p, Firefox m,p,a,s,
|
|
162
|
+
// Safari m,s,p,a, each a constant in its own source. Attached as a
|
|
163
|
+
// non-enumerable property so nothing that iterates the headers is affected.
|
|
164
|
+
var _order = [];
|
|
165
|
+
// And every field as decoded, repeats included: the object kept only the
|
|
166
|
+
// last of a repeated name (two cookie fields, one survived), and has no
|
|
167
|
+
// way to say a name came twice. The section is validated, and the request's
|
|
168
|
+
// headers built, from this list (validateFieldSection, http_common).
|
|
169
|
+
var _list = [];
|
|
170
|
+
// The section's size as RFC 9114 §4.2.2 measures it: decoded, name and
|
|
171
|
+
// value in bytes plus 32 per field — what SETTINGS_MAX_FIELD_SECTION_SIZE
|
|
172
|
+
// limits. Only the ENCODED size of a HEADERS frame was bounded, and a
|
|
173
|
+
// reference to a dynamic-table entry is one byte however long the entry:
|
|
174
|
+
// 64 KB of references to one 4 KB entry decoded to 256 MB, joined into a
|
|
175
|
+
// single field value (HPACK's "decompression bomb", which HTTP/2 here
|
|
176
|
+
// bounds by MAX_HEADER_LIST_SIZE the same way).
|
|
177
|
+
var sectionSize = 0;
|
|
178
|
+
function field(name, value) {
|
|
179
|
+
if (maxSectionSize) {
|
|
180
|
+
sectionSize += qpackEntrySize(name, value);
|
|
181
|
+
if (sectionSize > maxSectionSize) {
|
|
182
|
+
var big = new Error('QPACK: field section exceeds ' + maxSectionSize + ' bytes decoded');
|
|
183
|
+
big.excessive = true;
|
|
184
|
+
throw big;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
_order.push(name); _list.push([name, value]); if (name !== '__proto__') headers[name] = value;
|
|
188
|
+
}
|
|
189
|
+
|
|
188
190
|
while (pos < buf.length) {
|
|
189
191
|
var byte = buf[pos];
|
|
192
|
+
var idx, nameIdx, val, e;
|
|
190
193
|
|
|
191
194
|
if ((byte & 0x80) === 0x80) {
|
|
192
|
-
// Indexed
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
if (
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
195
|
+
// Indexed field line: 1 T xxxxxx
|
|
196
|
+
idx = readPrefixedInt(buf, 6, pos); if (!idx) throw new Error('QPACK: truncated field line');
|
|
197
|
+
pos = idx.next;
|
|
198
|
+
if (byte & 0x40) {
|
|
199
|
+
if (idx.value >= QPACK_STATIC.length) throw new Error('QPACK: static index ' + idx.value + ' out of range');
|
|
200
|
+
field(QPACK_STATIC[idx.value][0], QPACK_STATIC[idx.value][1]);
|
|
201
|
+
} else {
|
|
202
|
+
e = entryAt(base - 1 - idx.value);
|
|
203
|
+
field(e[0], e[1]);
|
|
202
204
|
}
|
|
203
205
|
|
|
204
206
|
} else if ((byte & 0xc0) === 0x40) {
|
|
205
|
-
// Literal with name
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
var
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
var name = null;
|
|
214
|
-
if (fromStatic && nameIdx.value < QPACK_STATIC.length) {
|
|
207
|
+
// Literal with name reference: 01 N T xxxx
|
|
208
|
+
nameIdx = readPrefixedInt(buf, 4, pos); if (!nameIdx) throw new Error('QPACK: truncated field line');
|
|
209
|
+
pos = nameIdx.next;
|
|
210
|
+
val = decodeStringLiteral(buf, 7, pos, true); if (!val) throw new Error('QPACK: truncated field line');
|
|
211
|
+
pos = val.next;
|
|
212
|
+
var name;
|
|
213
|
+
if (byte & 0x10) {
|
|
214
|
+
if (nameIdx.value >= QPACK_STATIC.length) throw new Error('QPACK: static name index ' + nameIdx.value + ' out of range');
|
|
215
215
|
name = QPACK_STATIC[nameIdx.value][0];
|
|
216
|
-
} else
|
|
217
|
-
|
|
218
|
-
if (di >= 0 && di < dynamicTable.length) name = dynamicTable[di][0];
|
|
216
|
+
} else {
|
|
217
|
+
name = entryAt(base - 1 - nameIdx.value)[0];
|
|
219
218
|
}
|
|
220
|
-
|
|
219
|
+
field(name, _text(val.bytes));
|
|
221
220
|
|
|
222
221
|
} else if ((byte & 0xe0) === 0x20) {
|
|
223
|
-
// Literal with literal name
|
|
224
|
-
var
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
222
|
+
// Literal with literal name: 001 N H xxx
|
|
223
|
+
var nameLit = decodeStringLiteral(buf, 3, pos, true); if (!nameLit) throw new Error('QPACK: truncated field line');
|
|
224
|
+
pos = nameLit.next;
|
|
225
|
+
val = decodeStringLiteral(buf, 7, pos, true); if (!val) throw new Error('QPACK: truncated field line');
|
|
226
|
+
pos = val.next;
|
|
227
|
+
field(_text(nameLit.bytes), _text(val.bytes));
|
|
228
|
+
|
|
229
|
+
} else if ((byte & 0xf0) === 0x10) {
|
|
230
|
+
// Indexed field line with post-base index: 0001 xxxx (§4.5.3)
|
|
231
|
+
idx = readPrefixedInt(buf, 4, pos); if (!idx) throw new Error('QPACK: truncated field line');
|
|
232
|
+
pos = idx.next;
|
|
233
|
+
e = entryAt(base + idx.value);
|
|
234
|
+
field(e[0], e[1]);
|
|
233
235
|
|
|
234
236
|
} else {
|
|
235
|
-
|
|
237
|
+
// Literal with post-base name reference: 0000 N xxx (§4.5.5)
|
|
238
|
+
nameIdx = readPrefixedInt(buf, 3, pos); if (!nameIdx) throw new Error('QPACK: truncated field line');
|
|
239
|
+
pos = nameIdx.next;
|
|
240
|
+
val = decodeStringLiteral(buf, 7, pos, true); if (!val) throw new Error('QPACK: truncated field line');
|
|
241
|
+
pos = val.next;
|
|
242
|
+
field(entryAt(base + nameIdx.value)[0], _text(val.bytes));
|
|
236
243
|
}
|
|
237
244
|
}
|
|
238
245
|
|
|
246
|
+
Object.defineProperty(headers, '_order', { value: _order, enumerable: false });
|
|
247
|
+
Object.defineProperty(headers, '_list', { value: _list, enumerable: false });
|
|
248
|
+
// For the Section Acknowledgment the decoder owes when this is non-zero.
|
|
249
|
+
Object.defineProperty(headers, '_ric', { value: ric, enumerable: false });
|
|
239
250
|
return headers;
|
|
240
251
|
}
|
|
241
252
|
|
|
242
|
-
function encodeQpackInt(value, prefixBits) {
|
|
243
|
-
var max = (1 << prefixBits) - 1;
|
|
244
|
-
if (value < max) return [value];
|
|
245
|
-
var bytes = [max];
|
|
246
|
-
value -= max;
|
|
247
|
-
while (value >= 128) { bytes.push((value & 0x7f) | 0x80); value >>= 7; }
|
|
248
|
-
bytes.push(value);
|
|
249
|
-
return bytes;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
// ---- Static table lookup maps (built once) ----
|
|
254
|
-
|
|
255
|
-
var _qpackFullMatch = {}; // "name\0value" → index
|
|
256
|
-
var _qpackNameMatch = {}; // "name" → first index
|
|
257
|
-
|
|
258
|
-
for (var _i = 0; _i < QPACK_STATIC.length; _i++) {
|
|
259
|
-
var _key = QPACK_STATIC[_i][0] + '\0' + QPACK_STATIC[_i][1];
|
|
260
|
-
if (!(_key in _qpackFullMatch)) _qpackFullMatch[_key] = _i;
|
|
261
|
-
if (!(QPACK_STATIC[_i][0] in _qpackNameMatch)) _qpackNameMatch[QPACK_STATIC[_i][0]] = _i;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
function buildQpackHeaderBlock(headers) {
|
|
266
|
-
var out = [0x00, 0x00]; // QPACK prefix: RIC=0, delta base=0
|
|
267
|
-
|
|
268
|
-
for (var name in headers) {
|
|
269
|
-
var lname = name.toLowerCase();
|
|
270
|
-
var value = String(headers[name]);
|
|
271
|
-
|
|
272
|
-
// (1) Try full match: name + value in static table → indexed (1 byte)
|
|
273
|
-
var fullKey = lname + '\0' + value;
|
|
274
|
-
if (fullKey in _qpackFullMatch) {
|
|
275
|
-
var idx = _qpackFullMatch[fullKey];
|
|
276
|
-
// Indexed: 1 1 NNNNNN (T=1 static, 6-bit prefix)
|
|
277
|
-
var enc = encodeQpackInt(idx, 6);
|
|
278
|
-
out.push(0xc0 | enc[0]);
|
|
279
|
-
for (var i = 1; i < enc.length; i++) out.push(enc[i]);
|
|
280
|
-
continue;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// (2) Try name match: name in static table → literal with name ref
|
|
284
|
-
if (lname in _qpackNameMatch) {
|
|
285
|
-
var nameIdx = _qpackNameMatch[lname];
|
|
286
|
-
// Literal with name reference: 01 N T PPPP (N=0, T=1 static, 4-bit prefix)
|
|
287
|
-
var nameEnc = encodeQpackInt(nameIdx, 4);
|
|
288
|
-
out.push(0x50 | nameEnc[0]);
|
|
289
|
-
for (var i = 1; i < nameEnc.length; i++) out.push(nameEnc[i]);
|
|
290
|
-
// Value: H=0 (no huffman), 7-bit length prefix
|
|
291
|
-
var valBytes = new TextEncoder().encode(value);
|
|
292
|
-
var valLenEnc = encodeQpackInt(valBytes.length, 7);
|
|
293
|
-
out.push(valLenEnc[0]);
|
|
294
|
-
for (var i = 1; i < valLenEnc.length; i++) out.push(valLenEnc[i]);
|
|
295
|
-
for (var i = 0; i < valBytes.length; i++) out.push(valBytes[i]);
|
|
296
|
-
continue;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// (3) No match: literal with literal name (fallback)
|
|
300
|
-
var nameBytes = new TextEncoder().encode(lname);
|
|
301
|
-
var nameLenEnc = encodeQpackInt(nameBytes.length, 3);
|
|
302
|
-
out.push(0x20 | nameLenEnc[0]);
|
|
303
|
-
for (var i = 1; i < nameLenEnc.length; i++) out.push(nameLenEnc[i]);
|
|
304
|
-
for (var i = 0; i < nameBytes.length; i++) out.push(nameBytes[i]);
|
|
305
|
-
var valBytes = new TextEncoder().encode(value);
|
|
306
|
-
var valLenEnc = encodeQpackInt(valBytes.length, 7);
|
|
307
|
-
out.push(valLenEnc[0]);
|
|
308
|
-
for (var i = 1; i < valLenEnc.length; i++) out.push(valLenEnc[i]);
|
|
309
|
-
for (var i = 0; i < valBytes.length; i++) out.push(valBytes[i]);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
return new Uint8Array(out);
|
|
313
|
-
}
|
|
314
253
|
|
|
315
254
|
|
|
316
255
|
// ============================================================
|
|
317
|
-
// H3 Frame build
|
|
256
|
+
// H3 Frame build
|
|
257
|
+
//
|
|
258
|
+
// Frames are read in place, from each stream's own buffer, as the bytes
|
|
259
|
+
// arrive: processBidiStream and processControlStream in H3Connection.
|
|
318
260
|
// ============================================================
|
|
319
261
|
|
|
320
262
|
function buildH3Frames(frames) {
|
|
@@ -327,49 +269,13 @@ function buildH3Frames(frames) {
|
|
|
327
269
|
return concatUint8Arrays(parts);
|
|
328
270
|
}
|
|
329
271
|
|
|
330
|
-
function extractH3Frames(chunks, fromOffset) {
|
|
331
|
-
if (!chunks || chunks.length === 0) return { frames: [], new_from_offset: fromOffset };
|
|
332
|
-
|
|
333
|
-
var buffers = [];
|
|
334
|
-
var acc = 0;
|
|
335
|
-
for (var i = 0; i < chunks.length; i++) {
|
|
336
|
-
var nextAcc = acc + chunks[i].length;
|
|
337
|
-
if (fromOffset < nextAcc) {
|
|
338
|
-
buffers.push(chunks[i].slice(fromOffset - acc));
|
|
339
|
-
for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
|
|
340
|
-
break;
|
|
341
|
-
}
|
|
342
|
-
acc = nextAcc;
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
if (buffers.length === 0) return { frames: [], new_from_offset: fromOffset };
|
|
346
|
-
|
|
347
|
-
var combined = concatUint8Arrays(buffers);
|
|
348
|
-
var offset = 0, frames = [];
|
|
349
|
-
|
|
350
|
-
while (offset < combined.length) {
|
|
351
|
-
var start = offset;
|
|
352
|
-
var ft = readVarInt(combined, offset);
|
|
353
|
-
if (!ft) break;
|
|
354
|
-
offset += ft.byteLength;
|
|
355
|
-
var len = readVarInt(combined, offset);
|
|
356
|
-
if (!len) { offset = start; break; }
|
|
357
|
-
offset += len.byteLength;
|
|
358
|
-
if (offset + len.value > combined.length) { offset = start; break; }
|
|
359
|
-
frames.push({ frame_type: ft.value, payload: combined.slice(offset, offset + len.value) });
|
|
360
|
-
offset += len.value;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
return { frames: frames, new_from_offset: fromOffset + offset };
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
|
|
367
272
|
// ============================================================
|
|
368
273
|
// H3 Settings
|
|
369
274
|
// ============================================================
|
|
370
275
|
|
|
371
276
|
var H3_SETTINGS_MAP = {
|
|
372
277
|
SETTINGS_QPACK_MAX_TABLE_CAPACITY: 0x01,
|
|
278
|
+
SETTINGS_QPACK_BLOCKED_STREAMS: 0x07,
|
|
373
279
|
SETTINGS_MAX_FIELD_SECTION_SIZE: 0x06,
|
|
374
280
|
SETTINGS_ENABLE_CONNECT_PROTOCOL: 0x08,
|
|
375
281
|
SETTINGS_H3_DATAGRAM: 0x33,
|
|
@@ -393,12 +299,38 @@ function buildSettingsPayload(settings) {
|
|
|
393
299
|
|
|
394
300
|
function parseSettingsPayload(buf) {
|
|
395
301
|
var out = {}, offset = 0;
|
|
302
|
+
// The same settings as an ordered list, on a non-enumerable property so it
|
|
303
|
+
// does not disturb anything that iterates the result. Order and unknown
|
|
304
|
+
// identifiers are both discarded by the object form and both matter for
|
|
305
|
+
// fingerprinting: which settings a peer sends, and in which order, is a
|
|
306
|
+
// property of its implementation.
|
|
307
|
+
var list = [];
|
|
308
|
+
var seen = {};
|
|
396
309
|
while (offset < buf.length) {
|
|
397
310
|
var id = readVarInt(buf, offset); if (!id) break; offset += id.byteLength;
|
|
398
311
|
var val = readVarInt(buf, offset); if (!val) break; offset += val.byteLength;
|
|
312
|
+
// RFC 9114 §7.2.4: an identifier twice, or one of HTTP/2's that HTTP/3
|
|
313
|
+
// reserves (0x02-0x05), is H3_SETTINGS_ERROR.
|
|
314
|
+
if (seen[id.value] || (id.value >= 0x02 && id.value <= 0x05)) {
|
|
315
|
+
var se = new Error('SETTINGS identifier 0x' + id.value.toString(16) + (seen[id.value] ? ' repeated' : ' is reserved'));
|
|
316
|
+
se.h3ErrorCode = 0x0109;
|
|
317
|
+
throw se;
|
|
318
|
+
}
|
|
319
|
+
seen[id.value] = true;
|
|
320
|
+
// Settings that are a yes or no: SETTINGS_ENABLE_CONNECT_PROTOCOL (RFC
|
|
321
|
+
// 8441 §3, by RFC 9220 §3 for HTTP/3) and SETTINGS_H3_DATAGRAM (RFC 9297
|
|
322
|
+
// §2.1.1: "...any value other than 0 or 1 ... as a connection error of
|
|
323
|
+
// type H3_SETTINGS_ERROR").
|
|
324
|
+
if ((id.value === 0x08 || id.value === 0x33) && val.value > 1) {
|
|
325
|
+
var ve = new Error('SETTINGS identifier 0x' + id.value.toString(16) + ' with value ' + val.value + ', not 0 or 1');
|
|
326
|
+
ve.h3ErrorCode = 0x0109;
|
|
327
|
+
throw ve;
|
|
328
|
+
}
|
|
399
329
|
var name = H3_ID_TO_NAME[id.value] || ('UNKNOWN_0x' + id.value.toString(16));
|
|
400
330
|
out[name] = val.value;
|
|
331
|
+
list.push({ id: id.value, value: val.value });
|
|
401
332
|
}
|
|
333
|
+
Object.defineProperty(out, '_ordered', { value: list, enumerable: false });
|
|
402
334
|
return out;
|
|
403
335
|
}
|
|
404
336
|
|
|
@@ -407,28 +339,25 @@ function parseSettingsPayload(buf) {
|
|
|
407
339
|
// QPACK Encoder Instructions (from peer's encoder stream)
|
|
408
340
|
// ============================================================
|
|
409
341
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
buffers.push(chunks[i].slice(fromOffset - acc));
|
|
419
|
-
for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
acc = nextAcc;
|
|
423
|
-
}
|
|
424
|
-
if (buffers.length === 0) return { instructions: [], new_from_offset: fromOffset };
|
|
425
|
-
|
|
426
|
-
var combined = concatUint8Arrays(buffers);
|
|
342
|
+
// Parse the encoder-stream instructions in `combined` — the stream's unparsed
|
|
343
|
+
// bytes, from its read position. Returns the complete instructions, how many
|
|
344
|
+
// bytes they took (consumed), and, when the last one is cut short, how many
|
|
345
|
+
// bytes it needs (need). It used to take the stream's chunks and an offset
|
|
346
|
+
// and join them itself; the one caller already hands it a single buffer.
|
|
347
|
+
function parseQpackEncoderInstructions(combined, maxLiteral) {
|
|
348
|
+
if (!combined || combined.length === 0) return { instructions: [], consumed: 0, need: 0 };
|
|
349
|
+
if (maxLiteral === undefined) maxLiteral = Infinity;
|
|
427
350
|
var pos = 0;
|
|
428
351
|
var instructions = [];
|
|
429
|
-
|
|
430
|
-
//
|
|
431
|
-
//
|
|
352
|
+
// When the last instruction is cut short: how many bytes, from its start,
|
|
353
|
+
// it needs before parsing it can succeed (0 = unknown, try on any byte).
|
|
354
|
+
// The caller waits for that many instead of re-parsing on every arrival —
|
|
355
|
+
// a literal arriving a byte at a time was re-joined and re-parsed whole
|
|
356
|
+
// each time: quadratic.
|
|
357
|
+
var needAbs = 0;
|
|
358
|
+
|
|
359
|
+
// Thin wrappers over the shared primitives that advance the local `pos` and
|
|
360
|
+
// return the value (or null on truncation), preserving call-site usage.
|
|
432
361
|
function safeVarInt(prefixBits) {
|
|
433
362
|
var r = readPrefixedInt(combined, prefixBits, pos);
|
|
434
363
|
if (!r) return null;
|
|
@@ -436,6 +365,26 @@ function parseQpackEncoderInstructions(chunks, fromOffset) {
|
|
|
436
365
|
return r.value;
|
|
437
366
|
}
|
|
438
367
|
|
|
368
|
+
// A string literal's length is checked before its bytes are awaited. No
|
|
369
|
+
// entry larger than the table capacity we advertised can ever be inserted
|
|
370
|
+
// (RFC 9204 §3.2.2), so a longer literal is an error now — waiting for it
|
|
371
|
+
// let a peer make us buffer an encoder stream of any size. Huffman can
|
|
372
|
+
// expand a byte to at most 30 bits, so its bound is 30/8 of that.
|
|
373
|
+
function safeLiteral(prefixBits) {
|
|
374
|
+
if (pos >= combined.length) return null;
|
|
375
|
+
var huffman = (combined[pos] & (1 << prefixBits)) !== 0;
|
|
376
|
+
var len = readPrefixedInt(combined, prefixBits, pos);
|
|
377
|
+
if (!len) return null;
|
|
378
|
+
var limit = huffman ? Math.ceil(maxLiteral * 30 / 8) + 1 : maxLiteral;
|
|
379
|
+
if (len.value > limit) {
|
|
380
|
+
throw new Error('literal of ' + len.value + ' bytes cannot fit a table of ' + maxLiteral);
|
|
381
|
+
}
|
|
382
|
+
if (len.next + len.value > combined.length) { needAbs = len.next + len.value; return null; }
|
|
383
|
+
var r = decodeStringLiteral(combined, prefixBits, pos, true);
|
|
384
|
+
pos = r.next;
|
|
385
|
+
return _text(r.bytes);
|
|
386
|
+
}
|
|
387
|
+
|
|
439
388
|
while (pos < combined.length) {
|
|
440
389
|
var startPos = pos;
|
|
441
390
|
var byte = combined[pos];
|
|
@@ -444,25 +393,13 @@ function parseQpackEncoderInstructions(chunks, fromOffset) {
|
|
|
444
393
|
// Insert with name reference
|
|
445
394
|
var fromStatic = (byte & 0x40) !== 0;
|
|
446
395
|
var nameIdx = safeVarInt(6); if (nameIdx === null) { pos = startPos; break; }
|
|
447
|
-
var
|
|
448
|
-
var valLen = safeVarInt(7); if (valLen === null) { pos = startPos; break; }
|
|
449
|
-
if (pos + valLen > combined.length) { pos = startPos; break; }
|
|
450
|
-
var valBytes = combined.slice(pos, pos + valLen); pos += valLen;
|
|
451
|
-
var value = valH ? huffDecode(valBytes) : new TextDecoder().decode(valBytes);
|
|
396
|
+
var value = safeLiteral(7); if (value === null) { pos = startPos; break; }
|
|
452
397
|
instructions.push({ type: 'insert_name_ref', from_static: fromStatic, name_index: nameIdx, value: value });
|
|
453
398
|
|
|
454
399
|
} else if ((byte & 0xc0) === 0x40) {
|
|
455
|
-
// Insert without name reference
|
|
456
|
-
var
|
|
457
|
-
var
|
|
458
|
-
if (pos + nameLen > combined.length) { pos = startPos; break; }
|
|
459
|
-
var nameBytes = combined.slice(pos, pos + nameLen); pos += nameLen;
|
|
460
|
-
var name = nameH ? huffDecode(nameBytes) : new TextDecoder().decode(nameBytes);
|
|
461
|
-
var valH = (combined[pos] & 0x80) !== 0;
|
|
462
|
-
var valLen = safeVarInt(7); if (valLen === null) { pos = startPos; break; }
|
|
463
|
-
if (pos + valLen > combined.length) { pos = startPos; break; }
|
|
464
|
-
var valBytes = combined.slice(pos, pos + valLen); pos += valLen;
|
|
465
|
-
var value = valH ? huffDecode(valBytes) : new TextDecoder().decode(valBytes);
|
|
400
|
+
// Insert without name reference: 01 H NNNNN, 5-bit prefix on the name
|
|
401
|
+
var name = safeLiteral(5); if (name === null) { pos = startPos; break; }
|
|
402
|
+
var value = safeLiteral(7); if (value === null) { pos = startPos; break; }
|
|
466
403
|
instructions.push({ type: 'insert_literal', name: name, value: value });
|
|
467
404
|
|
|
468
405
|
} else if ((byte & 0xe0) === 0x20) {
|
|
@@ -471,13 +408,83 @@ function parseQpackEncoderInstructions(chunks, fromOffset) {
|
|
|
471
408
|
instructions.push({ type: 'set_capacity', capacity: capacity });
|
|
472
409
|
|
|
473
410
|
} else {
|
|
474
|
-
break
|
|
411
|
+
// Duplicate: 000 xxxxx, relative index (§4.3.4). This used to `break`,
|
|
412
|
+
// which stopped reading the encoder stream for good: every instruction
|
|
413
|
+
// after it went unread, and our table fell permanently out of step with
|
|
414
|
+
// the peer's. Chrome duplicates entries it wants to keep alive.
|
|
415
|
+
var dupIdx = safeVarInt(5); if (dupIdx === null) { pos = startPos; break; }
|
|
416
|
+
instructions.push({ type: 'duplicate', index: dupIdx });
|
|
475
417
|
}
|
|
476
418
|
}
|
|
477
419
|
|
|
478
|
-
return { instructions: instructions,
|
|
420
|
+
return { instructions: instructions, consumed: pos,
|
|
421
|
+
need: needAbs > pos ? needAbs - pos : 0 };
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
// ============================================================
|
|
426
|
+
// Error codes (RFC 9114 §8.1, RFC 9204 §6)
|
|
427
|
+
//
|
|
428
|
+
// These live in QUIC's APPLICATION error space and must be sent in a 0x1d
|
|
429
|
+
// CONNECTION_CLOSE — see the note on QUICConnection.close().
|
|
430
|
+
//
|
|
431
|
+
// RFC 9114 splits failures into two kinds, and they are NOT interchangeable:
|
|
432
|
+
//
|
|
433
|
+
// Connection errors — the whole connection is unusable, because the shared
|
|
434
|
+
// state it depends on is now unknown. Every QPACK failure is here: the
|
|
435
|
+
// dynamic table is a running total of the peer's inserts, so one bad
|
|
436
|
+
// header block means every later block on every stream decodes to
|
|
437
|
+
// garbage. There is nothing to salvage per-stream. CONNECTION_CLOSE.
|
|
438
|
+
//
|
|
439
|
+
// Stream errors — one request is malformed but the connection is fine.
|
|
440
|
+
// RESET_STREAM / STOP_SENDING on that stream id, everything else
|
|
441
|
+
// continues. H3_MESSAGE_ERROR is the common case.
|
|
442
|
+
//
|
|
443
|
+
// Both are wired: a stream error is abortStream (STOP_SENDING and
|
|
444
|
+
// RESET_STREAM, both reliable control frames of quic_connection's —
|
|
445
|
+
// resetStream / stopSending, retransmitted until acknowledged) or
|
|
446
|
+
// stopReading (STOP_SENDING alone). H3 resets a stream by asking QUIC, which
|
|
447
|
+
// owns streams; the error code is an opaque varint QUIC never reads. HTTP/2
|
|
448
|
+
// has no layer beneath it to ask — its RST_STREAM is its own frame on one
|
|
449
|
+
// TCP byte stream — so the two share the idea, not the code.
|
|
450
|
+
// ============================================================
|
|
451
|
+
|
|
452
|
+
var H3_ERR = {
|
|
453
|
+
H3_NO_ERROR: 0x0100,
|
|
454
|
+
H3_GENERAL_PROTOCOL_ERROR: 0x0101,
|
|
455
|
+
H3_INTERNAL_ERROR: 0x0102,
|
|
456
|
+
H3_STREAM_CREATION_ERROR: 0x0103,
|
|
457
|
+
H3_CLOSED_CRITICAL_STREAM: 0x0104,
|
|
458
|
+
H3_FRAME_UNEXPECTED: 0x0105,
|
|
459
|
+
H3_FRAME_ERROR: 0x0106,
|
|
460
|
+
H3_EXCESSIVE_LOAD: 0x0107,
|
|
461
|
+
H3_ID_ERROR: 0x0108,
|
|
462
|
+
H3_SETTINGS_ERROR: 0x0109,
|
|
463
|
+
H3_MISSING_SETTINGS: 0x010a,
|
|
464
|
+
H3_REQUEST_REJECTED: 0x010b,
|
|
465
|
+
H3_REQUEST_CANCELLED: 0x010c,
|
|
466
|
+
H3_REQUEST_INCOMPLETE: 0x010d,
|
|
467
|
+
H3_MESSAGE_ERROR: 0x010e,
|
|
468
|
+
H3_CONNECT_ERROR: 0x010f,
|
|
469
|
+
H3_VERSION_FALLBACK: 0x0110,
|
|
470
|
+
|
|
471
|
+
QPACK_DECOMPRESSION_FAILED: 0x0200,
|
|
472
|
+
QPACK_ENCODER_STREAM_ERROR: 0x0201,
|
|
473
|
+
QPACK_DECODER_STREAM_ERROR: 0x0202
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// An exception from inside this code — a read past an end, a property of
|
|
477
|
+
// undefined — and not one of the protocol's: none of these is thrown on
|
|
478
|
+
// purpose on the paths that read a peer's bytes. It is our failure, and goes
|
|
479
|
+
// as H3_INTERNAL_ERROR (as HTTP/2 sends INTERNAL_ERROR for one), not under a
|
|
480
|
+
// code that blames the peer: H3_FRAME_ERROR, QPACK_DECOMPRESSION_FAILED.
|
|
481
|
+
function isInternalError(err) {
|
|
482
|
+
return err instanceof TypeError || err instanceof RangeError || err instanceof ReferenceError || err instanceof SyntaxError;
|
|
479
483
|
}
|
|
480
484
|
|
|
485
|
+
var H3_ERR_NAME = {};
|
|
486
|
+
for (var _ec in H3_ERR) H3_ERR_NAME[H3_ERR[_ec]] = _ec;
|
|
487
|
+
|
|
481
488
|
|
|
482
489
|
// ============================================================
|
|
483
490
|
// H3Connection — reactive pattern
|
|
@@ -488,6 +495,43 @@ function parseQpackEncoderInstructions(chunks, fromOffset) {
|
|
|
488
495
|
// 'http_body' (stream_id, data) — request body chunk
|
|
489
496
|
//
|
|
490
497
|
|
|
498
|
+
// The SETTINGS a connection with these local values sends.
|
|
499
|
+
function localSettings(c) {
|
|
500
|
+
var o = { SETTINGS_MAX_FIELD_SECTION_SIZE: c.local_max_header_size };
|
|
501
|
+
if (c.local_qpack_max_table_capacity > 0) {
|
|
502
|
+
o.SETTINGS_QPACK_MAX_TABLE_CAPACITY = c.local_qpack_max_table_capacity;
|
|
503
|
+
o.SETTINGS_QPACK_BLOCKED_STREAMS = QPACK_BLOCKED_STREAMS;
|
|
504
|
+
}
|
|
505
|
+
if (c.local_datagram_support) {
|
|
506
|
+
o.SETTINGS_H3_DATAGRAM = 1;
|
|
507
|
+
o.SETTINGS_ENABLE_CONNECT_PROTOCOL = 1;
|
|
508
|
+
}
|
|
509
|
+
if (c.local_webtransport) {
|
|
510
|
+
o.SETTINGS_ENABLE_WEBTRANSPORT = 1;
|
|
511
|
+
if (c.local_wt_max_sessions !== null) o.SETTINGS_WT_MAX_SESSIONS = c.local_wt_max_sessions;
|
|
512
|
+
}
|
|
513
|
+
return o;
|
|
514
|
+
}
|
|
515
|
+
function localValues(options) {
|
|
516
|
+
return {
|
|
517
|
+
local_max_header_size: 65536,
|
|
518
|
+
local_qpack_max_table_capacity: QPACK_DYNAMIC_TABLE ? QPACK_TABLE_BYTES : 0,
|
|
519
|
+
local_datagram_support: !!(options.enableWebTransport || options.enableDatagrams),
|
|
520
|
+
local_webtransport: !!options.enableWebTransport,
|
|
521
|
+
local_wt_max_sessions: options.maxWebTransportSessions > 0 ? Math.floor(options.maxWebTransportSessions) : null
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// 0-RTT (RFC 9114 §7.2.4.2): a client that remembered our SETTINGS may send
|
|
526
|
+
// early data under them — accepted only while they are the ones we send now.
|
|
527
|
+
// What a server built with these H3Connection options sends, as a string for
|
|
528
|
+
// the tickets' early-data context.
|
|
529
|
+
function settingsContext(options) {
|
|
530
|
+
var o = localSettings(localValues(options || {})), keys = Object.keys(o).sort(), parts = [];
|
|
531
|
+
for (var i = 0; i < keys.length; i++) parts.push(keys[i] + '=' + o[keys[i]]);
|
|
532
|
+
return 'h3:' + parts.join(',');
|
|
533
|
+
}
|
|
534
|
+
|
|
491
535
|
function H3Connection(options) {
|
|
492
536
|
if (!(this instanceof H3Connection)) return new H3Connection(options);
|
|
493
537
|
options = options || {};
|
|
@@ -495,17 +539,74 @@ function H3Connection(options) {
|
|
|
495
539
|
var ev = Emitter();
|
|
496
540
|
var quic = options.quicConnection;
|
|
497
541
|
|
|
542
|
+
// Set once a connection error has been raised. Everything downstream stops:
|
|
543
|
+
// the peer is getting a CONNECTION_CLOSE, and continuing to decode against a
|
|
544
|
+
// dynamic table we no longer trust can only produce more wrong headers.
|
|
545
|
+
var _fatal = false;
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Raise an HTTP/3 connection error: tell the owner, then close the QUIC
|
|
549
|
+
* connection with the application error code.
|
|
550
|
+
*
|
|
551
|
+
* Before this existed, a malformed header block threw out of the QPACK
|
|
552
|
+
* decoder, up through processBidiStream, into the quic.on('stream') listener
|
|
553
|
+
* — where Emitter's catch swallowed it. No error, no close, no RESET_STREAM.
|
|
554
|
+
* The request simply hung until the idle timeout, and the peer was never told
|
|
555
|
+
* why. Silently dropping a stream is the one response RFC 9114 does not allow.
|
|
556
|
+
*/
|
|
557
|
+
// Report-only escape hatch for the checks added alongside this error layer
|
|
558
|
+
// (strict Huffman padding, unresolvable table indices, post-base field
|
|
559
|
+
// lines). Every one of them turns something previously ignored into a closed
|
|
560
|
+
// connection — correct per RFC, but it means any latent bug they touch now
|
|
561
|
+
// presents as a hard failure against a real peer. With qpackLenient the
|
|
562
|
+
// condition is logged and the field skipped, restoring the old behaviour, so
|
|
563
|
+
// a regression can be isolated without reverting the whole layer.
|
|
564
|
+
var _lenient = options.qpackLenient === true;
|
|
565
|
+
|
|
566
|
+
function connectionError(code, reason) {
|
|
567
|
+
if (_fatal) return;
|
|
568
|
+
var name = H3_ERR_NAME[code] || ('0x' + code.toString(16));
|
|
569
|
+
|
|
570
|
+
if (_lenient) {
|
|
571
|
+
console.warn('[h3] qpackLenient: ignoring ' + name + ': ' + reason);
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
_fatal = true;
|
|
576
|
+
// Logged unconditionally, not behind DEBUG: this closes the connection, and
|
|
577
|
+
// the peer only ever sees a generic protocol error. One line here is the
|
|
578
|
+
// difference between a five-minute diagnosis and a packet capture.
|
|
579
|
+
console.error('[h3] connection error ' + name + ': ' + reason);
|
|
580
|
+
var err = new Error('HTTP/3 ' + name + ': ' + reason);
|
|
581
|
+
err.code = name;
|
|
582
|
+
err.errorCode = code;
|
|
583
|
+
ev.emit('error', err);
|
|
584
|
+
// `true` = application error space (0x1d). H3/QPACK codes are meaningless
|
|
585
|
+
// in the transport space.
|
|
586
|
+
try { quic.close(code, name, true); } catch (e) { /* already closing */ }
|
|
587
|
+
}
|
|
588
|
+
|
|
498
589
|
|
|
499
590
|
// ---- Context ----
|
|
500
591
|
|
|
592
|
+
var lv = localValues(options);
|
|
501
593
|
var context = {
|
|
502
594
|
isServer: options.isServer !== false,
|
|
503
595
|
|
|
504
|
-
// Local settings (what we advertise)
|
|
505
|
-
local_max_header_size:
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
596
|
+
// Local settings (what we advertise; localValues, as settingsContext sees them)
|
|
597
|
+
local_max_header_size: lv.local_max_header_size,
|
|
598
|
+
// QPACK_TABLE_BYTES. Measured against a real Chrome header set, a subresource request drops
|
|
599
|
+
// from 412 bytes to 80 once these are in the table. The encoder is the
|
|
600
|
+
// conservative one described in qpack.js, so this costs no risk of
|
|
601
|
+
// head-of-line blocking.
|
|
602
|
+
local_qpack_max_table_capacity: lv.local_qpack_max_table_capacity,
|
|
603
|
+
// SETTINGS_H3_DATAGRAM (RFC 9297): WebTransport needs it, and so does a
|
|
604
|
+
// CONNECT-UDP client (enableDatagrams) that is not WebTransport.
|
|
605
|
+
local_datagram_support: lv.local_datagram_support,
|
|
606
|
+
local_webtransport: lv.local_webtransport,
|
|
607
|
+
// How many WebTransport sessions a peer may have open at once on this
|
|
608
|
+
// connection (SETTINGS_WT_MAX_SESSIONS); null: no limit, and none sent.
|
|
609
|
+
local_wt_max_sessions: lv.local_wt_max_sessions,
|
|
509
610
|
|
|
510
611
|
// Remote settings (from peer's SETTINGS frame)
|
|
511
612
|
remote_max_header_size: 0,
|
|
@@ -516,6 +617,7 @@ function H3Connection(options) {
|
|
|
516
617
|
remote_qpack_dynamic_table: [],
|
|
517
618
|
remote_qpack_table_base_index: 0,
|
|
518
619
|
remote_qpack_table_capacity: 0,
|
|
620
|
+
remote_qpack_table_size: 0, // bytes, by name + value + 32
|
|
519
621
|
|
|
520
622
|
// Stream tracking
|
|
521
623
|
streams: {}, // stream_id → { chunks, from_offset, type }
|
|
@@ -528,6 +630,61 @@ function H3Connection(options) {
|
|
|
528
630
|
|
|
529
631
|
// ---- set_context ----
|
|
530
632
|
|
|
633
|
+
// ---- QPACK outbound state ----
|
|
634
|
+
//
|
|
635
|
+
// Declared here, above set_context, because set_context opens the table when
|
|
636
|
+
// the peer's SETTINGS arrives and would otherwise read an undefined `var`
|
|
637
|
+
// that is hoisted but not yet assigned — a silent no-op rather than an
|
|
638
|
+
// error, which is how the table stayed closed and nothing compressed.
|
|
639
|
+
//
|
|
640
|
+
// Separate from remote_qpack_dynamic_table, which is the PEER's table for
|
|
641
|
+
// what we receive. The two are independent: each side runs its own with its
|
|
642
|
+
// own capacity, and they are never in step.
|
|
643
|
+
var encoderTable = createQpackTable(0);
|
|
644
|
+
var encoderHistory = createQpackHistory(QPACK_TABLE_BYTES); // insert on second sighting
|
|
645
|
+
var encoderStreamId = null;
|
|
646
|
+
var decoderStreamId = null;
|
|
647
|
+
var qpackKnownReceived = 0; // insertions the peer has acknowledged
|
|
648
|
+
var qpackPendingAcks = 0; // insertions of THEIRS we owe an ack for
|
|
649
|
+
|
|
650
|
+
// ---- SETTINGS remembered for 0-RTT (RFC 9114 §7.2.4.2) ----
|
|
651
|
+
//
|
|
652
|
+
// A client keeps the server's SETTINGS with the sessions it saves; its
|
|
653
|
+
// early requests may then rely on them before this connection's arrive —
|
|
654
|
+
// datagrams, the field-section limit (the QPACK table stays closed until
|
|
655
|
+
// the real ones: using less is always compatible). With 0-RTT accepted,
|
|
656
|
+
// the server's new SETTINGS must not take back what was remembered: a
|
|
657
|
+
// known setting with a non-default value omitted, or one now smaller, is
|
|
658
|
+
// H3_SETTINGS_ERROR. Nothing was kept, and early requests went on the
|
|
659
|
+
// defaults with no check. Known identifiers only, as '0x…' keys.
|
|
660
|
+
function knownSettings(list) {
|
|
661
|
+
var out = {};
|
|
662
|
+
for (var i = 0; i < list.length; i++) if (H3_ID_TO_NAME[list[i].id]) out['0x' + list[i].id.toString(16)] = list[i].value;
|
|
663
|
+
return out;
|
|
664
|
+
}
|
|
665
|
+
var rememberedSettings = null;
|
|
666
|
+
function useRememberedSettings() {
|
|
667
|
+
var app = !context.isServer && quic && quic.earlyDataStatus === 'offered' ? quic.rememberedApplicationSettings : null;
|
|
668
|
+
if (!app || !app.h3 || typeof app.h3 !== 'object') return;
|
|
669
|
+
rememberedSettings = app.h3;
|
|
670
|
+
var u = {};
|
|
671
|
+
if (typeof rememberedSettings['0x6'] === 'number') u.remote_max_header_size = rememberedSettings['0x6'];
|
|
672
|
+
if (typeof rememberedSettings['0x33'] === 'number') u.remote_datagram_support = rememberedSettings['0x33'] > 0;
|
|
673
|
+
set_context(u);
|
|
674
|
+
}
|
|
675
|
+
function checkRememberedSettings(list) {
|
|
676
|
+
if (!rememberedSettings || !quic || quic.earlyDataStatus !== 'accepted') return;
|
|
677
|
+
var now = knownSettings(list);
|
|
678
|
+
for (var k in rememberedSettings) {
|
|
679
|
+
var was = rememberedSettings[k];
|
|
680
|
+
if (typeof was !== 'number' || !H3_ID_TO_NAME[parseInt(k, 16)]) continue;
|
|
681
|
+
if (!(k in now) ? was !== 0 : now[k] < was) {
|
|
682
|
+
throw h3Error(H3_ERR.H3_SETTINGS_ERROR, '0-RTT accepted, and SETTINGS ' + (H3_ID_TO_NAME[parseInt(k, 16)]) +
|
|
683
|
+
(!(k in now) ? ' omitted' : ' reduced') + ' (' + was + ' remembered' + ((k in now) ? ', ' + now[k] + ' now' : '') + ')');
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
531
688
|
function set_context(updates) {
|
|
532
689
|
if (!updates || typeof updates !== 'object') return;
|
|
533
690
|
|
|
@@ -541,6 +698,20 @@ function H3Connection(options) {
|
|
|
541
698
|
if ('remote_qpack_max_table_capacity' in updates && updates.remote_qpack_max_table_capacity !== context.remote_qpack_max_table_capacity) {
|
|
542
699
|
context.remote_qpack_max_table_capacity = updates.remote_qpack_max_table_capacity;
|
|
543
700
|
changed.remote_qpack_max_table_capacity = true;
|
|
701
|
+
|
|
702
|
+
// Only now may we insert anything. The capacity is the PEER's — it
|
|
703
|
+
// decides how much it is willing to hold for us — and inserting before
|
|
704
|
+
// it has said so would produce references it cannot resolve.
|
|
705
|
+
//
|
|
706
|
+
// The Set Dynamic Table Capacity instruction goes first on the encoder
|
|
707
|
+
// stream, before any insertion (§4.3.1).
|
|
708
|
+
var cap = QPACK_DYNAMIC_TABLE ? Math.min(updates.remote_qpack_max_table_capacity, QPACK_TABLE_BYTES) : 0;
|
|
709
|
+
if (cap > 0 && encoderTable.capacity === 0 && encoderStreamId !== null) {
|
|
710
|
+
encoderTable.setCapacity(cap);
|
|
711
|
+
quic.sendStream(encoderStreamId,
|
|
712
|
+
new Uint8Array(qpackEncodeSetCapacity(cap)), false);
|
|
713
|
+
if (DEBUG) console.log('[h3] qpack encoder table opened at ' + cap);
|
|
714
|
+
}
|
|
544
715
|
}
|
|
545
716
|
|
|
546
717
|
if ('remote_datagram_support' in updates) {
|
|
@@ -548,30 +719,16 @@ function H3Connection(options) {
|
|
|
548
719
|
changed.remote_datagram_support = true;
|
|
549
720
|
}
|
|
550
721
|
|
|
551
|
-
if ('remote_qpack_table_capacity' in updates) {
|
|
552
|
-
context.remote_qpack_table_capacity = updates.remote_qpack_table_capacity;
|
|
553
|
-
changed.remote_qpack_table_capacity = true;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
722
|
if ('remote_settings_received' in updates && !context.remote_settings_received) {
|
|
557
723
|
context.remote_settings_received = true;
|
|
558
724
|
changed.remote_settings_received = true;
|
|
559
725
|
}
|
|
560
726
|
|
|
561
|
-
if ('add_qpack_entries' in updates) {
|
|
562
|
-
var entries = updates.add_qpack_entries;
|
|
563
|
-
for (var i = 0; i < entries.length; i++) {
|
|
564
|
-
context.remote_qpack_dynamic_table.unshift(entries[i]);
|
|
565
|
-
context.remote_qpack_table_base_index++;
|
|
566
|
-
}
|
|
567
|
-
changed.qpack_table = true;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
727
|
// ---- Reactive ----
|
|
571
728
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
729
|
+
// (The peer's table — insertions, capacity, eviction, acknowledgements —
|
|
730
|
+
// is maintained by processQpackEncoderStream directly, one instruction at
|
|
731
|
+
// a time. It used to go through here in batches; see that function.)
|
|
575
732
|
|
|
576
733
|
if (changed.remote_settings_received) {
|
|
577
734
|
// Could trigger QPACK table capacity update etc.
|
|
@@ -579,40 +736,118 @@ function H3Connection(options) {
|
|
|
579
736
|
}
|
|
580
737
|
|
|
581
738
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
739
|
+
/**
|
|
740
|
+
* The peer's decoder stream: what it has acknowledged.
|
|
741
|
+
*
|
|
742
|
+
* The only instruction that matters to this encoder is Insert Count
|
|
743
|
+
* Increment. Each one raises the watermark below which an entry is safe to
|
|
744
|
+
* reference — and until an entry is below it, we send a literal instead.
|
|
745
|
+
* That single number is what makes this encoder non-blocking.
|
|
746
|
+
*
|
|
747
|
+
* Section acknowledgements and stream cancellations are parsed and skipped:
|
|
748
|
+
* they drive eviction accounting in an encoder that references
|
|
749
|
+
* unacknowledged entries, which this one never does.
|
|
750
|
+
*/
|
|
751
|
+
function processQpackDecoderStream(s) {
|
|
752
|
+
if (s.buffered === 0) return;
|
|
753
|
+
// Only the unparsed bytes; at most one instruction's worth (a few bytes)
|
|
754
|
+
// is ever left over between arrivals.
|
|
755
|
+
var buf = Buffer.from(copyBytes(s, s.buffered));
|
|
756
|
+
|
|
757
|
+
var r = qpackParseDecoderInstructions(buf, 0);
|
|
758
|
+
if (r.increments > 0) {
|
|
759
|
+
qpackKnownReceived += r.increments;
|
|
760
|
+
if (DEBUG) console.log('[h3] qpack acked ' + r.increments +
|
|
761
|
+
' (known=' + qpackKnownReceived + ')');
|
|
588
762
|
}
|
|
589
|
-
|
|
763
|
+
consumeBytes(s, r.consumed);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
function evictQpackTable(limit) {
|
|
767
|
+
// Oldest first, until what remains fits. Sizes in BYTES: measuring by
|
|
768
|
+
// string length, as this did, undercounts any non-ASCII header value and
|
|
769
|
+
// lets our table drift out of step with the peer's.
|
|
770
|
+
var entries = context.remote_qpack_dynamic_table;
|
|
771
|
+
if (limit === undefined) limit = context.remote_qpack_table_capacity;
|
|
772
|
+
while (context.remote_qpack_table_size > limit && entries.length > 0) {
|
|
590
773
|
var removed = entries.pop();
|
|
591
|
-
|
|
774
|
+
context.remote_qpack_table_size -= qpackEntrySize(removed[0], removed[1]);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function qpackEncoderStreamError(msg) {
|
|
779
|
+
var err = new Error('QPACK encoder stream: ' + msg);
|
|
780
|
+
err.h3ErrorCode = H3_ERR.QPACK_ENCODER_STREAM_ERROR;
|
|
781
|
+
return err;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// One insertion into the PEER's table, applied immediately — the next
|
|
785
|
+
// instruction on the same stream may refer to it by relative index.
|
|
786
|
+
function remoteTableInsert(name, value) {
|
|
787
|
+
var need = qpackEntrySize(name, value);
|
|
788
|
+
if (need > context.remote_qpack_table_capacity) {
|
|
789
|
+
throw qpackEncoderStreamError('entry of ' + need + ' bytes exceeds capacity ' +
|
|
790
|
+
context.remote_qpack_table_capacity);
|
|
592
791
|
}
|
|
792
|
+
evictQpackTable(context.remote_qpack_table_capacity - need);
|
|
793
|
+
context.remote_qpack_dynamic_table.unshift([name, value]);
|
|
794
|
+
context.remote_qpack_table_size += need;
|
|
795
|
+
context.remote_qpack_table_base_index++;
|
|
796
|
+
qpackPendingAcks++;
|
|
593
797
|
}
|
|
594
798
|
|
|
799
|
+
// On the encoder stream a relative index counts back from the most recent
|
|
800
|
+
// insertion (§3.2.5): 0 is the newest entry, which is also position 0 of
|
|
801
|
+
// this newest-first array. The old code computed an absolute index here
|
|
802
|
+
// instead, and named the inserted entry after the wrong one.
|
|
803
|
+
function remoteEntryByRelative(rel) {
|
|
804
|
+
var e = context.remote_qpack_dynamic_table[rel];
|
|
805
|
+
if (!e) throw qpackEncoderStreamError('relative index ' + rel + ' unresolvable');
|
|
806
|
+
return e;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Tell the peer what we now hold, so it may reference it.
|
|
810
|
+
function flushQpackAcks() {
|
|
811
|
+
if (qpackPendingAcks > 0 && decoderStreamId !== null) {
|
|
812
|
+
quic.sendStream(decoderStreamId, new Uint8Array(qpackEncodeAck(qpackPendingAcks)), false);
|
|
813
|
+
qpackPendingAcks = 0;
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
|
|
595
818
|
|
|
596
819
|
// ---- Send initial control + QPACK streams ----
|
|
597
820
|
|
|
821
|
+
/**
|
|
822
|
+
* Send queued insertions on the encoder stream.
|
|
823
|
+
*
|
|
824
|
+
* Before the header block that will eventually use them, and never for the
|
|
825
|
+
* block being sent now — nothing is referenced until acknowledged, so an
|
|
826
|
+
* insertion is always for a later request.
|
|
827
|
+
*/
|
|
828
|
+
function flushQpackInserts(inserts) {
|
|
829
|
+
if (!inserts.length || encoderStreamId === null) return;
|
|
830
|
+
var chunks = [];
|
|
831
|
+
for (var i = 0; i < inserts.length; i++) {
|
|
832
|
+
// Never evict: an entry that does not fit as things stand is not
|
|
833
|
+
// inserted. insert() would make room by dropping old entries, and one of
|
|
834
|
+
// those may still be referenced by a section the peer has not decoded
|
|
835
|
+
// yet — which RFC 9204 §2.1.1 forbids. (It used to do exactly that, and
|
|
836
|
+
// on "did not fit" also emptied our table without telling the peer.)
|
|
837
|
+
if (!encoderTable.fits(inserts[i].name, inserts[i].value)) continue;
|
|
838
|
+
encoderTable.insert(inserts[i].name, inserts[i].value);
|
|
839
|
+
chunks.push(qpackEncodeInsert(inserts[i].name, inserts[i].value));
|
|
840
|
+
}
|
|
841
|
+
if (chunks.length) {
|
|
842
|
+
quic.sendStream(encoderStreamId, new Uint8Array(Buffer.concat(chunks)), false);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
|
|
598
846
|
function sendControlStreams() {
|
|
599
847
|
if (context.control_streams_sent) return;
|
|
600
848
|
context.control_streams_sent = true;
|
|
601
849
|
|
|
602
|
-
var
|
|
603
|
-
SETTINGS_MAX_FIELD_SECTION_SIZE: context.local_max_header_size
|
|
604
|
-
};
|
|
605
|
-
if (context.local_qpack_max_table_capacity > 0) {
|
|
606
|
-
settingsObj.SETTINGS_QPACK_MAX_TABLE_CAPACITY = context.local_qpack_max_table_capacity;
|
|
607
|
-
}
|
|
608
|
-
if (context.local_datagram_support) {
|
|
609
|
-
settingsObj.SETTINGS_H3_DATAGRAM = 1;
|
|
610
|
-
settingsObj.SETTINGS_ENABLE_CONNECT_PROTOCOL = 1;
|
|
611
|
-
}
|
|
612
|
-
if (context.local_webtransport) {
|
|
613
|
-
settingsObj.SETTINGS_ENABLE_WEBTRANSPORT = 1;
|
|
614
|
-
}
|
|
615
|
-
var settings = buildSettingsPayload(settingsObj);
|
|
850
|
+
var settings = buildSettingsPayload(localSettings(context));
|
|
616
851
|
|
|
617
852
|
var controlFrame = buildH3Frames([{ frame_type: 0x04, payload: settings }]);
|
|
618
853
|
|
|
@@ -628,9 +863,18 @@ function H3Connection(options) {
|
|
|
628
863
|
var ctrlData = new Uint8Array(1 + controlFrame.byteLength);
|
|
629
864
|
ctrlData[0] = 0x00; // control stream type
|
|
630
865
|
ctrlData.set(controlFrame, 1);
|
|
866
|
+
// Ahead of every request and WebTransport stream (urgency -1, below
|
|
867
|
+
// RFC 9218's range): SETTINGS, GOAWAY, PRIORITY_UPDATE, and the QPACK
|
|
868
|
+
// instructions the header blocks on request streams depend on — a block
|
|
869
|
+
// that refers to an entry still waiting behind response bytes blocks.
|
|
870
|
+
quic.setStreamPriority(ctrlId, { urgency: -1 });
|
|
871
|
+
quic.setStreamPriority(encId, { urgency: -1 });
|
|
872
|
+
quic.setStreamPriority(decId, { urgency: -1 });
|
|
631
873
|
quic.sendStream(ctrlId, ctrlData, false);
|
|
632
874
|
quic.sendStream(encId, new Uint8Array([0x02]), false); // QPACK encoder
|
|
633
875
|
quic.sendStream(decId, new Uint8Array([0x03]), false); // QPACK decoder
|
|
876
|
+
encoderStreamId = encId;
|
|
877
|
+
decoderStreamId = decId;
|
|
634
878
|
}
|
|
635
879
|
|
|
636
880
|
|
|
@@ -650,90 +894,548 @@ function H3Connection(options) {
|
|
|
650
894
|
var _claimedStreams = new Set(); // streams owned by WebTransport (skip H3 processing)
|
|
651
895
|
var _wtSessions = new Set(); // active WT session stream IDs
|
|
652
896
|
var _wtStreams = {}; // streamId → { sessionId, isBidi }
|
|
897
|
+
var _wtLocalUni = {}; // our own WT uni streams: streamId → sessionId
|
|
898
|
+
|
|
899
|
+
// WebTransport streams that arrive before their session is accepted. The
|
|
900
|
+
// client may open them as soon as it has sent the CONNECT (draft-ietf-
|
|
901
|
+
// webtrans-http3 §4.6), and they travel on streams of their own, so one can
|
|
902
|
+
// be read before the CONNECT itself — or before the application has
|
|
903
|
+
// answered it. They used to be read as unknown streams and their bytes
|
|
904
|
+
// dropped, without a word to either side. Now they are held — their bytes
|
|
905
|
+
// in the stream's own buffer, uncredited, so the peer is held to one
|
|
906
|
+
// stream window each — until the session is accepted (then delivered as if
|
|
907
|
+
// just arrived) or turned down, or too many wait (then reset with
|
|
908
|
+
// WEBTRANSPORT_BUFFERED_STREAM_REJECTED, as the draft has it).
|
|
909
|
+
var WT_BUFFERED_STREAM_REJECTED = 0x3994bd84;
|
|
910
|
+
var MAX_HELD_WT_STREAMS = 64;
|
|
911
|
+
var _wtHeld = new Map(); // streamId → the session ID it names
|
|
912
|
+
var _wtConnects = new Set(); // WebTransport CONNECTs read, not yet answered (server)
|
|
913
|
+
|
|
914
|
+
// Could this name a session still to be accepted? A CONNECT read and not
|
|
915
|
+
// answered, or a client stream still open whose request is not read yet.
|
|
916
|
+
function wtSessionMayCome(sessionId) {
|
|
917
|
+
if (!context.isServer || sessionId % 4 !== 0) return false;
|
|
918
|
+
if (_wtConnects.has(sessionId)) return true;
|
|
919
|
+
if (!quic.isReceiving || !quic.isReceiving(sessionId)) return false;
|
|
920
|
+
var cs = context.streams[sessionId];
|
|
921
|
+
return !cs || (cs.type !== 'wt_held' && !cs.final_head_seen);
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function holdWtStream(s, sessionId) {
|
|
925
|
+
if (_wtHeld.size >= MAX_HELD_WT_STREAMS) { abortStream(s.sid, WT_BUFFERED_STREAM_REJECTED); return; }
|
|
926
|
+
s.type = 'wt_held';
|
|
927
|
+
_wtHeld.set(s.sid, sessionId);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// The session is accepted (go on) or not: its held streams, read again
|
|
931
|
+
// from their start, or reset.
|
|
932
|
+
function settleHeldWt(sessionId, accepted) {
|
|
933
|
+
var ids = [];
|
|
934
|
+
_wtHeld.forEach(function (sess, sid) { if (sess === sessionId) ids.push(sid); });
|
|
935
|
+
if (ids.length === 0) return;
|
|
936
|
+
if (!accepted) { for (var i = 0; i < ids.length; i++) abortStream(ids[i], WT_BUFFERED_STREAM_REJECTED); return; }
|
|
937
|
+
// After the caller's accept returns — the layer above finishes setting up
|
|
938
|
+
// the session (and the application its listeners) first.
|
|
939
|
+
queueMicrotask(function () {
|
|
940
|
+
for (var j = 0; j < ids.length; j++) {
|
|
941
|
+
var hs = context.streams[ids[j]];
|
|
942
|
+
if (!hs || _wtHeld.get(ids[j]) !== sessionId) continue;
|
|
943
|
+
_wtHeld.delete(ids[j]);
|
|
944
|
+
hs.type = null;
|
|
945
|
+
onStream(ids[j], new Uint8Array(0), false);
|
|
946
|
+
}
|
|
947
|
+
});
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
// Streams running an extended-CONNECT protocol that is NOT WebTransport —
|
|
951
|
+
// in practice WebSocket (RFC 9220). Their bytes are the inner protocol's,
|
|
952
|
+
// not HTTP/3 frames, so they bypass the frame parser entirely.
|
|
953
|
+
//
|
|
954
|
+
// Kept apart from _wtSessions on purpose. A WebTransport session makes every
|
|
955
|
+
// LATER stream that carries its id a member of that session; a WebSocket
|
|
956
|
+
// does not, and registering one as a session would make unrelated streams
|
|
957
|
+
// look like they belong to it.
|
|
958
|
+
var _tunnelStreams = new Set();
|
|
959
|
+
|
|
960
|
+
// Request bodies whose flow-control credit the application returns as it
|
|
961
|
+
// reads them (holdBody / creditBody / releaseBody). On the server every
|
|
962
|
+
// request stream's credit is held at the QUIC layer from its first byte
|
|
963
|
+
// (setCreditPolicy below); this layer credits what it consumes itself —
|
|
964
|
+
// frame headers, HEADERS, frames it skips — at once, and a body chunk
|
|
965
|
+
// either as it hands it up (the default) or, for a stream in this set, when
|
|
966
|
+
// the application has read it. The peer then sends a body no faster than
|
|
967
|
+
// it is read: a handler that had not read yet — waiting on an
|
|
968
|
+
// authorization check, say — had the whole upload held in memory, as fast
|
|
969
|
+
// as the client could send it.
|
|
970
|
+
var _bodyHeld = new Set();
|
|
971
|
+
// The peer's control and QPACK streams, by type → stream ID.
|
|
972
|
+
var _criticalStreams = {};
|
|
973
|
+
//
|
|
974
|
+
// WebTransport streams the same way: the client's unidirectional streams
|
|
975
|
+
// are held too from their first byte, since what a stream is shows only in
|
|
976
|
+
// those bytes — and our own bidirectional ones, which only WebTransport
|
|
977
|
+
// opens, for what the client sends back on them. Control and QPACK streams (and ones of unknown type) give
|
|
978
|
+
// their credit back the moment they are recognised; a WebTransport stream,
|
|
979
|
+
// bidirectional or not, keeps it held, and h3_server returns it as the
|
|
980
|
+
// application reads. Credited on delivery, a stream nobody read was taken
|
|
981
|
+
// in whole: 40 MB from a browser, all of it in memory.
|
|
982
|
+
//
|
|
983
|
+
// The same on either side — a WebTransport client reads the server's
|
|
984
|
+
// streams as the server reads the client's: every stream the peer can send
|
|
985
|
+
// on, so all but our own unidirectional ones.
|
|
986
|
+
if (quic.setCreditPolicy) {
|
|
987
|
+
var ownUni = context.isServer ? 3 : 2;
|
|
988
|
+
quic.setCreditPolicy(function (sid) { return (sid % 4) !== ownUni; });
|
|
989
|
+
}
|
|
990
|
+
function creditRecv(sid, n) { if (n > 0 && quic.creditStream) quic.creditStream(sid, n); }
|
|
991
|
+
function releaseRecvCredit(sid) { if (quic.releaseStreamCredit) quic.releaseStreamCredit(sid); }
|
|
992
|
+
|
|
993
|
+
// Bytes on a stream we have decided is a tunnel but have not yet accepted.
|
|
994
|
+
//
|
|
995
|
+
// The window is real and unavoidable: the client learns the CONNECT was
|
|
996
|
+
// accepted only when our 2xx reaches it, but nothing stops it sending frames
|
|
997
|
+
// the moment it sent the CONNECT — and an eager client (or a slow network in
|
|
998
|
+
// the other direction) does exactly that. Those bytes are not HTTP/3 frames,
|
|
999
|
+
// so they cannot go to the frame parser; they are held here and replayed, in
|
|
1000
|
+
// order, the instant the tunnel opens.
|
|
1001
|
+
var _tunnelPending = {};
|
|
1002
|
+
var _tunnelPendingBytes = {};
|
|
1003
|
+
var _tunnelPendingIds = new Set();
|
|
1004
|
+
// QUIC returns flow-control credit as it delivers bytes, so bytes held
|
|
1005
|
+
// here are not bounded by it: a client that never waits for the 2xx could
|
|
1006
|
+
// fill memory. Past this, the tunnel is refused.
|
|
1007
|
+
var MAX_TUNNEL_PENDING = 1024 * 1024; // per stream
|
|
1008
|
+
var MAX_TUNNEL_PENDING_TOTAL = 4 * 1024 * 1024; // per connection
|
|
1009
|
+
var _tunnelPendingTotal = 0;
|
|
1010
|
+
var _tunnelPendingFin = new Set();
|
|
1011
|
+
|
|
1012
|
+
// Frames on the control stream are read whole (SETTINGS, GOAWAY); no real
|
|
1013
|
+
// one comes near this. Anything else there is skipped as it arrives.
|
|
1014
|
+
var MAX_CONTROL_FRAME = 16384;
|
|
1015
|
+
|
|
1016
|
+
// ── Stream lifecycle: one exit ────────────────────────────────────────
|
|
1017
|
+
//
|
|
1018
|
+
// releaseStream is the ONLY place per-stream state is dropped, and every
|
|
1019
|
+
// way a stream can end comes through it:
|
|
1020
|
+
//
|
|
1021
|
+
// FIN, on whichever path read the stream (HTTP/3 frames, a tunnel, a
|
|
1022
|
+
// WebTransport stream — including a FIN that arrived with its prefix —,
|
|
1023
|
+
// a claimed or an ignored stream) → the 'stream' handler
|
|
1024
|
+
// RESET_STREAM from the peer → quic 'stream_reset'
|
|
1025
|
+
// cancelled or refused by us → abortStream / stopReading
|
|
1026
|
+
// its WebTransport session ended → endWtSession → abortStream
|
|
1027
|
+
// the connection closed → quic 'close'
|
|
1028
|
+
//
|
|
1029
|
+
// It drops only what this layer holds for the stream's RECEIVING side; a
|
|
1030
|
+
// response still being written is QUIC's, and unaffected. Idempotent. The
|
|
1031
|
+
// state used to be dropped piecemeal at each of those sites, each with its
|
|
1032
|
+
// own subset — which is how entries came to outlive their streams.
|
|
1033
|
+
function releaseStream(sid, keepBody) {
|
|
1034
|
+
sid = Number(sid);
|
|
1035
|
+
// Credit still held for it goes back — unless the stream ended normally
|
|
1036
|
+
// (keepBody) and the application still has its body to read: then it is
|
|
1037
|
+
// returned as that is read, or by releaseBody.
|
|
1038
|
+
if (!(keepBody && _bodyHeld.has(sid))) { _bodyHeld.delete(sid); releaseRecvCredit(sid); }
|
|
1039
|
+
delete context.streams[sid];
|
|
1040
|
+
_blocked.delete(sid);
|
|
1041
|
+
_prioPending.delete(sid);
|
|
1042
|
+
_wtHeld.delete(sid);
|
|
1043
|
+
// A CONNECT that ends unanswered: the streams waiting for it will not
|
|
1044
|
+
// get a session.
|
|
1045
|
+
if (_wtConnects.delete(sid) || !_wtSessions.has(sid)) settleHeldWt(sid, false);
|
|
1046
|
+
_tunnelStreams.delete(sid);
|
|
1047
|
+
_tunnelPendingIds.delete(sid);
|
|
1048
|
+
dropTunnelPending(sid);
|
|
1049
|
+
delete _wtStreams[sid];
|
|
1050
|
+
_claimedStreams.delete(sid);
|
|
1051
|
+
delete _wtLocalUni[sid];
|
|
1052
|
+
endWtSession(sid); // a CONNECT stream's end is its session's end
|
|
1053
|
+
}
|
|
653
1054
|
|
|
654
|
-
|
|
1055
|
+
// The connection is gone: every stream with it. No frames are sent — there
|
|
1056
|
+
// is no one to send them to — but the layer above still hears of each
|
|
1057
|
+
// What this layer listens to on the QUIC connection, so that it can stop
|
|
1058
|
+
// (detach): a client whose 0-RTT the server refused starts HTTP/3 over on
|
|
1059
|
+
// the same connection, and the layer that spoke early must not hear it.
|
|
1060
|
+
var _quicListeners = [];
|
|
1061
|
+
function listenQuic(name, fn) { _quicListeners.push([name, fn]); quic.on(name, fn); }
|
|
1062
|
+
|
|
1063
|
+
// WebTransport session ending, through the same path as any other end.
|
|
1064
|
+
var _connClosed = false;
|
|
1065
|
+
listenQuic('close', function () {
|
|
1066
|
+
_connClosed = true;
|
|
1067
|
+
var ids = new Set();
|
|
1068
|
+
[context.streams, _wtStreams, _tunnelPending, _wtLocalUni].forEach(function (m) {
|
|
1069
|
+
for (var k in m) ids.add(Number(k));
|
|
1070
|
+
});
|
|
1071
|
+
[_tunnelStreams, _tunnelPendingIds, _claimedStreams, _wtSessions, _bodyHeld].forEach(function (set) {
|
|
1072
|
+
set.forEach(function (k) { ids.add(Number(k)); });
|
|
1073
|
+
});
|
|
1074
|
+
ids.forEach(releaseStream);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
function dropTunnelPending(sid) {
|
|
1078
|
+
if (_tunnelPendingBytes[sid]) _tunnelPendingTotal -= _tunnelPendingBytes[sid];
|
|
1079
|
+
delete _tunnelPending[sid];
|
|
1080
|
+
delete _tunnelPendingBytes[sid];
|
|
1081
|
+
_tunnelPendingFin.delete(sid);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Bytes held for a tunnel, handed over in order — with the FIN, if it came.
|
|
1085
|
+
function replayTunnelPending(sid) {
|
|
1086
|
+
var pend = _tunnelPending[sid];
|
|
1087
|
+
var fin = _tunnelPendingFin.has(sid);
|
|
1088
|
+
dropTunnelPending(sid);
|
|
1089
|
+
if (pend) for (var i = 0; i < pend.length; i++) ev.emit('tunnel_data', sid, pend[i], false);
|
|
1090
|
+
if (fin) { ev.emit('tunnel_data', sid, new Uint8Array(0), true); releaseStream(sid); }
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
// A WebTransport session is over (its CONNECT stream ended or was reset).
|
|
1094
|
+
// Its streams go with it: reset and stopped with WEBTRANSPORT_SESSION_GONE
|
|
1095
|
+
// (draft-ietf-webtrans-http3 §4.6), and reported so whoever holds them can
|
|
1096
|
+
// destroy them. They used to stay in _wtStreams / _claimedStreams, routed
|
|
1097
|
+
// to a session that no longer existed, until each one happened to end.
|
|
1098
|
+
var WT_SESSION_GONE = 0x170d7b68;
|
|
1099
|
+
function endWtSession(sessionId) {
|
|
1100
|
+
if (!_wtSessions.delete(sessionId)) return;
|
|
1101
|
+
var children = [];
|
|
1102
|
+
for (var k in _wtStreams) {
|
|
1103
|
+
if (_wtStreams[k].sessionId === sessionId) children.push(Number(k));
|
|
1104
|
+
}
|
|
1105
|
+
for (var u in _wtLocalUni) {
|
|
1106
|
+
if (_wtLocalUni[u] === sessionId) children.push(Number(u));
|
|
1107
|
+
}
|
|
1108
|
+
for (var i = 0; i < children.length; i++) {
|
|
1109
|
+
if (_connClosed) releaseStream(children[i]);
|
|
1110
|
+
else abortStream(children[i], WT_SESSION_GONE);
|
|
1111
|
+
}
|
|
1112
|
+
ev.emit('wt_session_closed', sessionId, children);
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// A request stream announces itself as soon as it is one — its first bytes
|
|
1116
|
+
// read as HTTP/3, not WebTransport — before its HEADERS frame has even
|
|
1117
|
+
// arrived whole: the position of h2's 'stream_open', for the same reasons.
|
|
1118
|
+
// A refusal here costs nothing decoded, and the clock on a request's head
|
|
1119
|
+
// (h3_server's headersTimeout) starts with its first byte, not with a
|
|
1120
|
+
// HEADERS frame that a slow peer may never finish sending. It was announced
|
|
1121
|
+
// at that frame's decode, so a HEADERS trickled a byte at a time was
|
|
1122
|
+
// invisible for as long as it lasted.
|
|
1123
|
+
// Emitted unconditionally: a listenerCount guard once stood here, when
|
|
1124
|
+
// h3's emitter had no such method, and the event never fired at all.
|
|
1125
|
+
function announceRequestStream(s) {
|
|
1126
|
+
if (s.announced) return;
|
|
1127
|
+
s.announced = true;
|
|
1128
|
+
ev.emit('stream_open', Number(s.sid));
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
// A stream WE end because of what the peer sent on it — a stream error
|
|
1132
|
+
// (RFC 9114 §8). The stream is cancelled both ways, and reported: a
|
|
1133
|
+
// request above may already exist for it, and would otherwise wait for
|
|
1134
|
+
// an end that is never coming.
|
|
1135
|
+
function streamError(sid, code, reason) {
|
|
1136
|
+
if (DEBUG) console.log('[h3] stream error on ' + sid + ' (' + (H3_ERR_NAME[code] || code) + '): ' + reason);
|
|
1137
|
+
abortStream(sid, code);
|
|
1138
|
+
ev.emit('stream_error', sid, code, reason);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// Streams WE cancelled (abortStream). The peer's replies to that — it MUST
|
|
1142
|
+
// answer our STOP_SENDING with a RESET_STREAM (RFC 9000 §3.5) — are not the
|
|
1143
|
+
// peer cancelling anything, and must not be reported as if it were: a rate
|
|
1144
|
+
// limiter counting cancellations would charge clients for our refusals.
|
|
1145
|
+
// Bounded; a stream's echo arrives within a round trip or two.
|
|
1146
|
+
var _abortedHere = new Set();
|
|
1147
|
+
function rememberAborted(sid) {
|
|
1148
|
+
_abortedHere.add(sid);
|
|
1149
|
+
if (_abortedHere.size > 4096) _abortedHere.delete(_abortedHere.values().next().value);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
// The peer reset its sending part (RESET_STREAM). For a request stream that
|
|
1153
|
+
// is the client cancelling the request — RFC 9114 §4.1.1 — and whoever is
|
|
1154
|
+
// handling it needs to know. The WebTransport session is passed along for
|
|
1155
|
+
// a WT stream, so it can be routed to its session.
|
|
1156
|
+
listenQuic('stream_reset', function (streamId, code) {
|
|
655
1157
|
var sid = Number(streamId);
|
|
1158
|
+
var wt = _wtStreams[sid];
|
|
1159
|
+
var wasParsing = sid in context.streams;
|
|
1160
|
+
// A client's request stream reset before its HEADERS came: there is no
|
|
1161
|
+
// request, and no one above to end the response side — it stayed open
|
|
1162
|
+
// for good, its slot of the client's stream limit with it (and what the
|
|
1163
|
+
// transport keeps per stream until both sides end). Ended here, as a
|
|
1164
|
+
// cancelled request (RFC 9114 §4.1.1). Not a WebTransport stream: its
|
|
1165
|
+
// session's.
|
|
1166
|
+
var noRequest = context.isServer && (sid & 3) === 0 && !wt && !_wtHeld.has(sid) &&
|
|
1167
|
+
!(wasParsing && context.streams[sid].headers_seen);
|
|
1168
|
+
releaseStream(sid);
|
|
1169
|
+
if (noRequest && quic.resetStream) quic.resetStream(sid, H3_ERR.H3_REQUEST_CANCELLED);
|
|
1170
|
+
if (_abortedHere.has(sid)) return;
|
|
1171
|
+
if (wasParsing) sendStreamCancel(sid);
|
|
1172
|
+
ev.emit('stream_reset', sid, code, wt ? wt.sessionId : null);
|
|
1173
|
+
});
|
|
1174
|
+
|
|
1175
|
+
// The peer will not read what we send (STOP_SENDING). quic_connection has
|
|
1176
|
+
// already answered with RESET_STREAM, as RFC 9000 §3.5 requires. This is
|
|
1177
|
+
// how a browser cancels a GET: its own side of the stream is already
|
|
1178
|
+
// finished, so there is nothing for it to reset — STOP_SENDING is the
|
|
1179
|
+
// whole cancellation.
|
|
1180
|
+
listenQuic('stop_sending', function (streamId, code) {
|
|
1181
|
+
var sid = Number(streamId);
|
|
1182
|
+
if (_abortedHere.has(sid)) return;
|
|
1183
|
+
ev.emit('stop_sending', sid, code);
|
|
1184
|
+
});
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Cancel a stream, both directions — RFC 9114 §4.1.1: "reset the sending
|
|
1188
|
+
* parts of streams and abort reading on the receiving parts". For a request
|
|
1189
|
+
* the server declines to process, `code` is H3_REQUEST_REJECTED (the
|
|
1190
|
+
* client may safely retry it); for one abandoned part way,
|
|
1191
|
+
* H3_REQUEST_CANCELLED.
|
|
1192
|
+
*/
|
|
1193
|
+
function abortStream(streamId, code) {
|
|
1194
|
+
var sid = Number(streamId);
|
|
1195
|
+
rememberAborted(sid);
|
|
1196
|
+
var stopped = quic.stopSending ? quic.stopSending(sid, code) : false;
|
|
1197
|
+
if (quic.resetStream) quic.resetStream(sid, code);
|
|
1198
|
+
if (stopped) sendStreamCancel(sid);
|
|
1199
|
+
releaseStream(sid);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Stop reading a stream, leaving our sending part alone — for a stream
|
|
1203
|
+
// whose response is already written (a refused tunnel: the 4xx/5xx went
|
|
1204
|
+
// out with FIN, and resetting would discard it).
|
|
1205
|
+
function stopReading(streamId, code) {
|
|
1206
|
+
var sid = Number(streamId);
|
|
1207
|
+
rememberAborted(sid);
|
|
1208
|
+
var stopped = quic.stopSending ? quic.stopSending(sid, code) : false;
|
|
1209
|
+
if (stopped) sendStreamCancel(sid);
|
|
1210
|
+
releaseStream(sid);
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// RFC 9204 §2.2.2.2: reading of a request stream was abandoned (by us, or
|
|
1214
|
+
// by the peer's reset), so the peer's encoder must hear that any section
|
|
1215
|
+
// on it will never be acknowledged — or the entries it references can
|
|
1216
|
+
// never be evicted. Header sections live on client-initiated
|
|
1217
|
+
// bidirectional streams, whichever side we are.
|
|
1218
|
+
function sendStreamCancel(sid) {
|
|
1219
|
+
if ((sid % 4) !== 0) return;
|
|
1220
|
+
if (context.local_qpack_max_table_capacity > 0 && decoderStreamId !== null) {
|
|
1221
|
+
quic.sendStream(decoderStreamId, new Uint8Array(qpackEncodeStreamCancel(sid)), false);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
listenQuic('stream', onStream);
|
|
1226
|
+
function onStream(streamId, data, fin) {
|
|
1227
|
+
if (_fatal) return; // connection is closing; decoding further is pointless
|
|
1228
|
+
var sid = Number(streamId);
|
|
1229
|
+
|
|
1230
|
+
// An extended-CONNECT tunnel: after the CONNECT is accepted, everything on
|
|
1231
|
+
// this stream belongs to the protocol running inside it. RFC 9220 §3 —
|
|
1232
|
+
// WebSocket frames, not DATA frames, and reading them as HTTP/3 would fail
|
|
1233
|
+
// on the first opcode.
|
|
1234
|
+
if (_tunnelStreams.has(sid)) {
|
|
1235
|
+
// Bytes that arrived before the application accepted the CONNECT
|
|
1236
|
+
// (normally replayed at accept already). They go first, in order.
|
|
1237
|
+
if (_tunnelPending[sid]) replayTunnelPending(sid);
|
|
1238
|
+
ev.emit('tunnel_data', sid, data, fin);
|
|
1239
|
+
if (fin) releaseStream(sid);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
656
1242
|
|
|
657
1243
|
// Route data for already-detected WT streams
|
|
658
1244
|
if (sid in _wtStreams) {
|
|
659
1245
|
var ws = _wtStreams[sid];
|
|
660
1246
|
ev.emit('wt_data', ws.sessionId, sid, data, fin);
|
|
661
|
-
|
|
1247
|
+
// Nothing arrives after a FIN (quic_connection's recv_final), so the
|
|
1248
|
+
// claim is not needed past it. Kept, both grew by one entry for every
|
|
1249
|
+
// WebTransport stream the connection ever carried.
|
|
1250
|
+
if (fin) releaseStream(sid);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
if (_tunnelPendingIds.has(sid)) {
|
|
1255
|
+
// Marked as a tunnel-to-be by the application, not yet accepted.
|
|
1256
|
+
if (!_tunnelPending[sid]) { _tunnelPending[sid] = []; _tunnelPendingBytes[sid] = 0; }
|
|
1257
|
+
if (data && data.byteLength) {
|
|
1258
|
+
_tunnelPendingBytes[sid] += data.byteLength;
|
|
1259
|
+
_tunnelPendingTotal += data.byteLength;
|
|
1260
|
+
if (_tunnelPendingBytes[sid] > MAX_TUNNEL_PENDING || _tunnelPendingTotal > MAX_TUNNEL_PENDING_TOTAL) {
|
|
1261
|
+
streamError(sid, H3_ERR.H3_EXCESSIVE_LOAD, 'tunnel data held before the CONNECT was accepted exceeds the limit');
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
_tunnelPending[sid].push(data);
|
|
1265
|
+
}
|
|
1266
|
+
// The FIN is kept too: nothing more arrives after it, so it has to be
|
|
1267
|
+
// replayed at accept, not on a next delivery that never comes.
|
|
1268
|
+
if (fin) _tunnelPendingFin.add(sid);
|
|
662
1269
|
return;
|
|
663
1270
|
}
|
|
664
1271
|
|
|
665
|
-
|
|
1272
|
+
// Owned by the WebTransport client (webtransport.js reads it itself);
|
|
1273
|
+
// released at its FIN like any other — it used to be kept for good.
|
|
1274
|
+
if (_claimedStreams.has(sid)) { if (fin) releaseStream(sid); return; }
|
|
666
1275
|
if (DEBUG) console.log('[h3] stream ' + streamId + ' data=' + data.byteLength + ' fin=' + fin);
|
|
667
1276
|
if (!(streamId in context.streams)) {
|
|
668
|
-
context.streams[streamId] = { chunks: [], from_offset: 0, type: null, fin_received: false
|
|
1277
|
+
context.streams[streamId] = { sid: sid, chunks: [], from_offset: 0, type: null, fin_received: false,
|
|
1278
|
+
buffered: 0, // bytes held past from_offset
|
|
1279
|
+
data_left: 0, skip_left: 0,
|
|
1280
|
+
headers_seen: false, need: 0 };
|
|
1281
|
+
if (context.isServer && sid % 4 === 0 && sid > _maxPeerBidi) _maxPeerBidi = sid;
|
|
669
1282
|
}
|
|
670
1283
|
|
|
671
1284
|
var s = context.streams[streamId];
|
|
672
|
-
s.chunks.push(data);
|
|
1285
|
+
if (data && data.byteLength) { s.chunks.push(data); s.buffered += data.byteLength; }
|
|
673
1286
|
if (fin) s.fin_received = true;
|
|
674
1287
|
|
|
675
|
-
// Identify stream type
|
|
1288
|
+
// Identify the stream type — from its first bytes, however QUIC splits
|
|
1289
|
+
// them. The type used to be read from the first DELIVERY: one that was
|
|
1290
|
+
// empty (a zero-length STREAM frame) left the stream typeless for good,
|
|
1291
|
+
// holding every byte sent on it; a WebTransport prefix split across two
|
|
1292
|
+
// deliveries was read as HTTP/3 frames. Only the first few bytes are
|
|
1293
|
+
// copied, never the whole buffer.
|
|
676
1294
|
if (s.type === null) {
|
|
677
|
-
var isUni = (
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1295
|
+
var isUni = (sid % 4) >= 2;
|
|
1296
|
+
var head = copyBytes(s, Math.min(s.buffered, 16));
|
|
1297
|
+
var st = readVarInt(head, 0);
|
|
1298
|
+
|
|
1299
|
+
if (!st) {
|
|
1300
|
+
// Not even the first varint yet.
|
|
1301
|
+
if (!s.fin_received) return;
|
|
1302
|
+
if (isUni) { releaseStream(sid); return; } // empty: nothing to do
|
|
1303
|
+
s.type = 'bidi'; // processBidiStream reports what is wrong with it
|
|
1304
|
+
announceRequestStream(s);
|
|
1305
|
+
} else {
|
|
1306
|
+
// ---- WT stream detection ----
|
|
1307
|
+
var wtType = isUni ? 0x54 : 0x41;
|
|
1308
|
+
if (context.local_webtransport && st.value === wtType) {
|
|
1309
|
+
var sidResult = readVarInt(head, st.byteLength);
|
|
1310
|
+
if (!sidResult && !s.fin_received) return; // the session ID is still to come
|
|
1311
|
+
if (sidResult && !_wtSessions.has(sidResult.value)) {
|
|
1312
|
+
// Not a session (yet): held for one that may come, refused
|
|
1313
|
+
// for one that will not.
|
|
1314
|
+
if (wtSessionMayCome(sidResult.value)) holdWtStream(s, sidResult.value);
|
|
1315
|
+
else abortStream(sid, WT_BUFFERED_STREAM_REJECTED);
|
|
1316
|
+
return;
|
|
1317
|
+
}
|
|
1318
|
+
if (sidResult) {
|
|
1319
|
+
var sessionId = sidResult.value;
|
|
1320
|
+
var prefixLen = st.byteLength + sidResult.byteLength;
|
|
1321
|
+
consumeBytes(s, prefixLen);
|
|
1322
|
+
var remaining = s.buffered > 0 ? copyBytes(s, s.buffered) : new Uint8Array(0);
|
|
1323
|
+
|
|
1324
|
+
delete context.streams[sid]; // no longer parsed as HTTP/3: a WT stream now
|
|
1325
|
+
// The prefix was ours, and consumeBytes credited it; the rest is
|
|
1326
|
+
// the application's, credited as it reads it (h3_server).
|
|
1327
|
+
_claimedStreams.add(sid);
|
|
1328
|
+
_wtStreams[sid] = { sessionId: sessionId, isBidi: !isUni };
|
|
1329
|
+
|
|
1330
|
+
if (DEBUG) console.log('[h3] WT stream detected: stream=' + sid + ' session=' + sessionId + ' bidi=' + !isUni + ' dataLen=' + remaining.byteLength);
|
|
1331
|
+
ev.emit('wt_stream', sessionId, sid, remaining, s.fin_received, !isUni);
|
|
1332
|
+
if (s.fin_received) releaseStream(sid); // the FIN came with the prefix
|
|
1333
|
+
return;
|
|
706
1334
|
}
|
|
707
1335
|
}
|
|
708
|
-
}
|
|
709
1336
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
if (
|
|
714
|
-
else if (
|
|
715
|
-
|
|
716
|
-
|
|
1337
|
+
if (isUni) {
|
|
1338
|
+
if (st.value === 0x00) s.type = 'control';
|
|
1339
|
+
else if (st.value === 0x02) s.type = 'qpack_enc';
|
|
1340
|
+
else if (st.value === 0x03) s.type = 'qpack_dec';
|
|
1341
|
+
else if (st.value === 0x01) {
|
|
1342
|
+
// A push stream (§4.6, §6.2.2). Only a server pushes: one from a
|
|
1343
|
+
// client is H3_STREAM_CREATION_ERROR. A client allowed none (it
|
|
1344
|
+
// sends no MAX_PUSH_ID): "...receipt of a push stream as a
|
|
1345
|
+
// connection error of type H3_ID_ERROR when no MAX_PUSH_ID frame
|
|
1346
|
+
// has been sent".
|
|
1347
|
+
releaseRecvCredit(sid);
|
|
1348
|
+
if (context.isServer) connectionError(H3_ERR.H3_STREAM_CREATION_ERROR, 'a push stream from the client');
|
|
1349
|
+
else connectionError(H3_ERR.H3_ID_ERROR, 'a push stream, none allowed');
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
else s.type = 'unknown';
|
|
1353
|
+
if (s.type !== 'unknown') consumeBytes(s, st.byteLength);
|
|
1354
|
+
// Not a WebTransport stream: its credit is not the application's
|
|
1355
|
+
// to return (see setCreditPolicy above).
|
|
1356
|
+
releaseRecvCredit(sid);
|
|
1357
|
+
// One of each per peer (RFC 9114 §6.2.1, RFC 9204 §4.2): a second
|
|
1358
|
+
// control or QPACK stream is H3_STREAM_CREATION_ERROR. Read, a
|
|
1359
|
+
// second encoder stream fed the same dynamic table from two
|
|
1360
|
+
// directions (nginx CVE-2026-42530 was a reopened encoder stream).
|
|
1361
|
+
if (s.type !== 'unknown') {
|
|
1362
|
+
if (_criticalStreams[s.type] !== undefined && _criticalStreams[s.type] !== sid) {
|
|
1363
|
+
connectionError(H3_ERR.H3_STREAM_CREATION_ERROR, 'a second ' + s.type + ' stream');
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
_criticalStreams[s.type] = sid;
|
|
1367
|
+
}
|
|
1368
|
+
} else if ((sid % 4) === 1) {
|
|
1369
|
+
// Server-initiated bidirectional: HTTP/3 itself never opens one
|
|
1370
|
+
// (RFC 9114 §6.1) — only an extension such as WebTransport, whose
|
|
1371
|
+
// client side (webtransport.js) reads these streams itself. Parsed
|
|
1372
|
+
// as request frames, a WT payload starting with 0x00 read as DATA
|
|
1373
|
+
// before HEADERS and closed the whole connection.
|
|
1374
|
+
s.type = 'unknown';
|
|
1375
|
+
releaseRecvCredit(sid); // not read: nothing will return its credit
|
|
1376
|
+
} else {
|
|
1377
|
+
s.type = 'bidi';
|
|
1378
|
+
announceRequestStream(s);
|
|
717
1379
|
}
|
|
718
|
-
} else {
|
|
719
|
-
s.type = 'bidi';
|
|
720
1380
|
}
|
|
721
1381
|
}
|
|
722
1382
|
|
|
723
|
-
// Process by type
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
1383
|
+
// Process by type.
|
|
1384
|
+
//
|
|
1385
|
+
// Each processor decodes attacker-controlled bytes, so each can throw:
|
|
1386
|
+
// a bad prefixed integer, an invalid Huffman code, a truncated literal.
|
|
1387
|
+
// The catch turns that into the connection error RFC 9114 requires,
|
|
1388
|
+
// instead of letting it unwind into Emitter's swallow.
|
|
1389
|
+
try {
|
|
1390
|
+
if ((s.type === 'control' || s.type === 'qpack_enc' || s.type === 'qpack_dec') && s.fin_received) {
|
|
1391
|
+
// RFC 9114 §6.2.1, RFC 9204 §4.2: these live as long as the connection.
|
|
1392
|
+
throw h3Error(H3_ERR.H3_CLOSED_CRITICAL_STREAM, 'the peer closed its ' + s.type + ' stream');
|
|
1393
|
+
}
|
|
1394
|
+
if (s.type === 'control') {
|
|
1395
|
+
processControlStream(s);
|
|
1396
|
+
|
|
1397
|
+
} else if (s.type === 'qpack_enc') {
|
|
1398
|
+
processQpackEncoderStream(s);
|
|
1399
|
+
|
|
1400
|
+
} else if (s.type === 'qpack_dec') {
|
|
1401
|
+
processQpackDecoderStream(s);
|
|
1402
|
+
|
|
1403
|
+
} else if (s.type === 'bidi') {
|
|
1404
|
+
processBidiStream(streamId, s);
|
|
1405
|
+
|
|
1406
|
+
} else if (s.type === 'unknown') {
|
|
1407
|
+
// A unidirectional stream of a type we do not know — GREASE, or an
|
|
1408
|
+
// extension we do not speak. RFC 9114 §6.2: its data is ignored. It
|
|
1409
|
+
// used to be kept all the same, every byte, for the connection's life.
|
|
1410
|
+
s.chunks.length = 0;
|
|
1411
|
+
s.from_offset = 0;
|
|
1412
|
+
s.buffered = 0;
|
|
1413
|
+
if (s.fin_received) releaseStream(sid);
|
|
1414
|
+
}
|
|
729
1415
|
|
|
730
|
-
|
|
731
|
-
|
|
1416
|
+
// The processors read s.chunks from s.from_offset onward; what lies
|
|
1417
|
+
// before it is done with. Drop those chunks and rebase the offset, so
|
|
1418
|
+
// the array holds only bytes not yet parsed. Kept, every arrival walked
|
|
1419
|
+
// the whole history to find its place and re-copied the unparsed part
|
|
1420
|
+
// of it: O(n^2) over a large upload, and over the connection's life on
|
|
1421
|
+
// the control and QPACK streams, which never end.
|
|
1422
|
+
while (s.chunks.length > 0 && s.from_offset >= s.chunks[0].length) {
|
|
1423
|
+
s.from_offset -= s.chunks[0].length;
|
|
1424
|
+
s.chunks.shift();
|
|
1425
|
+
}
|
|
1426
|
+
} catch (err) {
|
|
1427
|
+
// A code already chosen at the throw site (see decodeHeaders below) wins;
|
|
1428
|
+
// an exception of our own is H3_INTERNAL_ERROR (isInternalError), and
|
|
1429
|
+
// counted as the QUIC layer counts its own; anything else is a framing
|
|
1430
|
+
// failure on whichever stream we were reading.
|
|
1431
|
+
if (!err.h3ErrorCode && isInternalError(err) && quic.countInternalError) quic.countInternalError(err);
|
|
1432
|
+
connectionError(err.h3ErrorCode || (isInternalError(err) ? H3_ERR.H3_INTERNAL_ERROR : H3_ERR.H3_FRAME_ERROR),
|
|
1433
|
+
err.message || 'stream processing failed');
|
|
732
1434
|
}
|
|
733
|
-
}
|
|
1435
|
+
}
|
|
734
1436
|
|
|
735
1437
|
|
|
736
|
-
|
|
1438
|
+
listenQuic('connect', function () {
|
|
737
1439
|
sendControlStreams();
|
|
738
1440
|
});
|
|
739
1441
|
|
|
@@ -745,110 +1447,685 @@ function H3Connection(options) {
|
|
|
745
1447
|
|
|
746
1448
|
// ---- Stream processors ----
|
|
747
1449
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
if ('SETTINGS_QPACK_MAX_TABLE_CAPACITY' in settings) {
|
|
760
|
-
updates.remote_qpack_max_table_capacity = settings['SETTINGS_QPACK_MAX_TABLE_CAPACITY'];
|
|
761
|
-
}
|
|
762
|
-
if ('SETTINGS_MAX_FIELD_SECTION_SIZE' in settings) {
|
|
763
|
-
updates.remote_max_header_size = settings['SETTINGS_MAX_FIELD_SECTION_SIZE'];
|
|
764
|
-
}
|
|
765
|
-
if ('SETTINGS_H3_DATAGRAM' in settings) {
|
|
766
|
-
updates.remote_datagram_support = settings['SETTINGS_H3_DATAGRAM'] > 0;
|
|
767
|
-
}
|
|
1450
|
+
// The control stream is read frame by frame, like a request stream. It
|
|
1451
|
+
// used to re-join everything unparsed on every arrival and wait for each
|
|
1452
|
+
// frame whole: a frame of an unknown type (legal — RFC 9114 §9 says to
|
|
1453
|
+
// ignore it) announcing a huge length made us buffer all of it, with a full
|
|
1454
|
+
// copy per packet. Now frames we do not read are skipped as they arrive,
|
|
1455
|
+
// and the ones we do read are bounded.
|
|
1456
|
+
function h3Error(code, msg) {
|
|
1457
|
+
var e = new Error(msg);
|
|
1458
|
+
e.h3ErrorCode = code;
|
|
1459
|
+
return e;
|
|
1460
|
+
}
|
|
768
1461
|
|
|
769
|
-
|
|
1462
|
+
// Frame types of HTTP/2 that HTTP/3 reserves (RFC 9114 §7.2.8, §11.2.1):
|
|
1463
|
+
// PRIORITY, PING, WINDOW_UPDATE, CONTINUATION. On any stream:
|
|
1464
|
+
// H3_FRAME_UNEXPECTED.
|
|
1465
|
+
function reservedH2Frame(t) { return t === 0x02 || t === 0x06 || t === 0x08 || t === 0x09; }
|
|
1466
|
+
|
|
1467
|
+
// Server push (RFC 9114 §4.6). Neither side here takes part in it: the
|
|
1468
|
+
// client sends no MAX_PUSH_ID, so no push ID is allowed it; the server
|
|
1469
|
+
// sends no PUSH_PROMISE, so no push ID exists to cancel. What each side
|
|
1470
|
+
// received of the peer's push frames, to hold them to their own rules.
|
|
1471
|
+
var _peerMaxPushId = -1; // server: the highest MAX_PUSH_ID received
|
|
1472
|
+
var _peerGoawayId = -1; // the last GOAWAY's identifier
|
|
1473
|
+
|
|
1474
|
+
// A frame payload that is one varint and nothing else (CANCEL_PUSH,
|
|
1475
|
+
// GOAWAY, MAX_PUSH_ID): its value, or H3_FRAME_ERROR (§7.1: "...contain
|
|
1476
|
+
// additional bytes after the identified fields or a frame payload that
|
|
1477
|
+
// terminates before the end of the identified fields").
|
|
1478
|
+
function oneVarint(payload, what) {
|
|
1479
|
+
var v = readVarInt(payload, 0);
|
|
1480
|
+
if (!v || v.byteLength !== payload.length) throw h3Error(H3_ERR.H3_FRAME_ERROR, what + ' payload is not one integer');
|
|
1481
|
+
return v.value;
|
|
1482
|
+
}
|
|
770
1483
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
1484
|
+
function processControlStream(s) {
|
|
1485
|
+
while (true) {
|
|
1486
|
+
if (s.skip_left > 0) {
|
|
1487
|
+
var skipNow = Math.min(s.skip_left, s.buffered);
|
|
1488
|
+
if (skipNow === 0) return;
|
|
1489
|
+
consumeBytes(s, skipNow);
|
|
1490
|
+
s.skip_left -= skipNow;
|
|
1491
|
+
continue;
|
|
778
1492
|
}
|
|
1493
|
+
if (s.buffered === 0) return;
|
|
1494
|
+
var head = copyBytes(s, Math.min(s.buffered, 16));
|
|
1495
|
+
var ft = readVarInt(head, 0);
|
|
1496
|
+
if (!ft) return;
|
|
1497
|
+
var flen = readVarInt(head, ft.byteLength);
|
|
1498
|
+
if (!flen) return;
|
|
1499
|
+
var headLen = ft.byteLength + flen.byteLength;
|
|
1500
|
+
|
|
1501
|
+
// RFC 9114 §7.2.1, §7.2.2, §7.2.5: DATA, HEADERS and PUSH_PROMISE on
|
|
1502
|
+
// the control stream are H3_FRAME_UNEXPECTED.
|
|
1503
|
+
if (ft.value === 0x00 || ft.value === 0x01 || ft.value === 0x05 || reservedH2Frame(ft.value)) {
|
|
1504
|
+
throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'frame type 0x' + ft.value.toString(16) + ' on the control stream');
|
|
1505
|
+
}
|
|
1506
|
+
// RFC 9114 §6.2.1, §7.2.4: SETTINGS first, and once.
|
|
1507
|
+
if (!s.settings_seen && ft.value !== 0x04) throw h3Error(H3_ERR.H3_MISSING_SETTINGS, 'the control stream did not begin with SETTINGS');
|
|
1508
|
+
if (s.settings_seen && ft.value === 0x04) throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'a second SETTINGS frame');
|
|
1509
|
+
// MAX_PUSH_ID from a server (§7.2.7): H3_FRAME_UNEXPECTED, on sight.
|
|
1510
|
+
if (ft.value === 0x0d && !context.isServer) {
|
|
1511
|
+
throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'MAX_PUSH_ID from the server');
|
|
1512
|
+
}
|
|
1513
|
+
if (ft.value !== 0x04 && ft.value !== 0x07 && ft.value !== 0x03 && ft.value !== 0x0d &&
|
|
1514
|
+
ft.value !== PRIORITY_UPDATE_REQUEST && ft.value !== PRIORITY_UPDATE_PUSH) {
|
|
1515
|
+
consumeBytes(s, headLen);
|
|
1516
|
+
s.skip_left = flen.value;
|
|
1517
|
+
continue;
|
|
1518
|
+
}
|
|
1519
|
+
if (flen.value > MAX_CONTROL_FRAME) {
|
|
1520
|
+
throw h3Error(H3_ERR.H3_EXCESSIVE_LOAD, 'control frame of ' + flen.value + ' bytes');
|
|
1521
|
+
}
|
|
1522
|
+
if (s.buffered < headLen + flen.value) return;
|
|
1523
|
+
consumeBytes(s, headLen);
|
|
1524
|
+
var payload = copyBytes(s, flen.value);
|
|
1525
|
+
consumeBytes(s, flen.value);
|
|
1526
|
+
if (ft.value === 0x04) s.settings_seen = true;
|
|
1527
|
+
handleControlFrame(ft.value, payload);
|
|
1528
|
+
if (_fatal) return;
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
function handleControlFrame(frameType, payload) {
|
|
1533
|
+
if (DEBUG) console.log('[h3] control: frame 0x' + frameType.toString(16) + ' len=' + payload.length);
|
|
1534
|
+
if (frameType === 0x04) {
|
|
1535
|
+
var settings = parseSettingsPayload(payload);
|
|
1536
|
+
checkRememberedSettings(settings._ordered || []);
|
|
1537
|
+
// Kept with the sessions saved from now on (a client's).
|
|
1538
|
+
if (!context.isServer && quic && quic.rememberApplicationSettings) quic.rememberApplicationSettings({ h3: knownSettings(settings._ordered || []) });
|
|
1539
|
+
var updates = { remote_settings_received: true };
|
|
1540
|
+
|
|
1541
|
+
if ('SETTINGS_QPACK_MAX_TABLE_CAPACITY' in settings) {
|
|
1542
|
+
updates.remote_qpack_max_table_capacity = settings['SETTINGS_QPACK_MAX_TABLE_CAPACITY'];
|
|
1543
|
+
}
|
|
1544
|
+
if ('SETTINGS_MAX_FIELD_SECTION_SIZE' in settings) {
|
|
1545
|
+
updates.remote_max_header_size = settings['SETTINGS_MAX_FIELD_SECTION_SIZE'];
|
|
1546
|
+
}
|
|
1547
|
+
if ('SETTINGS_H3_DATAGRAM' in settings) {
|
|
1548
|
+
updates.remote_datagram_support = settings['SETTINGS_H3_DATAGRAM'] > 0;
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
set_context(updates);
|
|
1552
|
+
|
|
1553
|
+
// Surfaced separately from the context update: this is an observation
|
|
1554
|
+
// about the peer, not a setting that changes our behaviour.
|
|
1555
|
+
ev.emit('peer_settings', settings._ordered || []);
|
|
1556
|
+
|
|
1557
|
+
} else if (frameType === 0x07) {
|
|
1558
|
+
// GOAWAY (RFC 9114 §5.2): the peer will not process requests/pushes
|
|
1559
|
+
// with an identifier >= this value. Surface it so consumers (e.g. the
|
|
1560
|
+
// agent) can stop sending new requests on this connection.
|
|
1561
|
+
var gid = oneVarint(payload, 'GOAWAY');
|
|
1562
|
+
// From a server, a client-initiated bidirectional stream ID — "...of
|
|
1563
|
+
// any other type as a connection error of type H3_ID_ERROR"; from a
|
|
1564
|
+
// client, a push ID. Either way it may only stay or go down: "...an
|
|
1565
|
+
// identifier greater than that sent in a previous GOAWAY frame ...
|
|
1566
|
+
// MUST treat this as a connection error of type H3_ID_ERROR".
|
|
1567
|
+
if (!context.isServer && gid % 4 !== 0) throw h3Error(H3_ERR.H3_ID_ERROR, 'GOAWAY with stream ' + gid + ', not a request stream');
|
|
1568
|
+
if (_peerGoawayId >= 0 && gid > _peerGoawayId) throw h3Error(H3_ERR.H3_ID_ERROR, 'GOAWAY went up, ' + _peerGoawayId + ' to ' + gid);
|
|
1569
|
+
_peerGoawayId = gid;
|
|
1570
|
+
if (DEBUG) console.log('[h3] ← GOAWAY id=' + gid);
|
|
1571
|
+
ev.emit('goaway', gid);
|
|
1572
|
+
} else if (frameType === 0x03) {
|
|
1573
|
+
// CANCEL_PUSH (§7.2.3). At a server, of a push it never promised — it
|
|
1574
|
+
// promises none: "...a push ID that has not yet been mentioned by a
|
|
1575
|
+
// PUSH_PROMISE frame, this MUST be treated as a connection error of
|
|
1576
|
+
// type H3_ID_ERROR". At a client, of a push ID above the maximum it
|
|
1577
|
+
// allowed — it allowed none: H3_ID_ERROR.
|
|
1578
|
+
var cp = oneVarint(payload, 'CANCEL_PUSH');
|
|
1579
|
+
throw h3Error(H3_ERR.H3_ID_ERROR, 'CANCEL_PUSH for push ' + cp + (context.isServer ? ', never promised' : ', none allowed'));
|
|
1580
|
+
} else if (frameType === 0x0d) {
|
|
1581
|
+
// MAX_PUSH_ID at the server (§7.2.7; the client's case is refused
|
|
1582
|
+
// above): "...a MAX_PUSH_ID frame that contains a smaller value than
|
|
1583
|
+
// previously received MUST be treated as a connection error of type
|
|
1584
|
+
// H3_ID_ERROR." Kept, though no push follows.
|
|
1585
|
+
var mp = oneVarint(payload, 'MAX_PUSH_ID');
|
|
1586
|
+
if (mp < _peerMaxPushId) throw h3Error(H3_ERR.H3_ID_ERROR, 'MAX_PUSH_ID went down, ' + _peerMaxPushId + ' to ' + mp);
|
|
1587
|
+
_peerMaxPushId = mp;
|
|
1588
|
+
} else {
|
|
1589
|
+
handlePriorityUpdate(frameType, payload);
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
// ---- Priorities (RFC 9218) ────────────────────────────────────────────
|
|
1594
|
+
//
|
|
1595
|
+
// The client's signals, read here and handed to QUIC's scheduler as the
|
|
1596
|
+
// peer's (setStreamPriority's fromPeer): the `priority` header of a
|
|
1597
|
+
// request, and PRIORITY_UPDATE on its control stream, which replaces it.
|
|
1598
|
+
// What the application sets for a response (h3_server's res.setPriority)
|
|
1599
|
+
// is the server's decision, and a signal does not override it. A request
|
|
1600
|
+
// with no signal keeps the scheduler's default: urgency 3, sharing turns.
|
|
1601
|
+
//
|
|
1602
|
+
// A PRIORITY_UPDATE may arrive before its request (§7.1 — they travel on
|
|
1603
|
+
// different streams): held until the request's head is read, then applied
|
|
1604
|
+
// over it. Bounded, oldest out; a client has no reason to run far ahead.
|
|
1605
|
+
var PRIORITY_UPDATE_REQUEST = 0xF0700, PRIORITY_UPDATE_PUSH = 0xF0701;
|
|
1606
|
+
var PRIORITY_PENDING_MAX = 128;
|
|
1607
|
+
var _prioPending = new Map(); // sid -> Priority Field Value
|
|
1608
|
+
var _maxPeerBidi = -1; // highest request stream the client has opened
|
|
1609
|
+
|
|
1610
|
+
function handlePriorityUpdate(frameType, payload) {
|
|
1611
|
+
// §7.2: the server sends none.
|
|
1612
|
+
if (!context.isServer) throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'PRIORITY_UPDATE from the server');
|
|
1613
|
+
var id = readVarInt(payload, 0);
|
|
1614
|
+
if (!id) throw h3Error(H3_ERR.H3_FRAME_ERROR, 'PRIORITY_UPDATE without an element ID');
|
|
1615
|
+
// §7.2: no push was ever allowed (we send no MAX_PUSH_ID), so any push ID
|
|
1616
|
+
// is beyond the limit; a request ID must be a client-initiated
|
|
1617
|
+
// bidirectional stream.
|
|
1618
|
+
if (frameType === PRIORITY_UPDATE_PUSH) throw h3Error(H3_ERR.H3_ID_ERROR, 'PRIORITY_UPDATE for a push');
|
|
1619
|
+
var sid = id.value;
|
|
1620
|
+
if (sid % 4 !== 0) throw h3Error(H3_ERR.H3_ID_ERROR, 'PRIORITY_UPDATE for stream ' + sid + ', not a request stream');
|
|
1621
|
+
var value = Buffer.from(payload.subarray(id.byteLength)).toString('latin1');
|
|
1622
|
+
var s = context.streams[sid];
|
|
1623
|
+
if (sid > _maxPeerBidi || (s && s.type !== 'bidi') || (s && !s.final_head_seen)) {
|
|
1624
|
+
_prioPending.delete(sid);
|
|
1625
|
+
_prioPending.set(sid, value);
|
|
1626
|
+
if (_prioPending.size > PRIORITY_PENDING_MAX) _prioPending.delete(_prioPending.keys().next().value);
|
|
1627
|
+
return;
|
|
779
1628
|
}
|
|
1629
|
+
quic.setStreamPriority(sid, parsePriority(value), true); // false for a stream already over: nothing to do
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// A request's head is read: its signals, the header first and any update
|
|
1633
|
+
// that came ahead of it over it.
|
|
1634
|
+
function applyRequestPriority(sid, headers) {
|
|
1635
|
+
if (headers.priority != null) quic.setStreamPriority(sid, parsePriority(headers.priority), true);
|
|
1636
|
+
if (_prioPending.has(sid)) {
|
|
1637
|
+
quic.setStreamPriority(sid, parsePriority(_prioPending.get(sid)), true);
|
|
1638
|
+
_prioPending.delete(sid);
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
// The client's side: change a request's priority after it was sent (§7).
|
|
1643
|
+
function sendPriorityUpdate(streamId, p) {
|
|
1644
|
+
if (context.isServer) return false;
|
|
1645
|
+
sendControlStreams();
|
|
1646
|
+
var field = new Uint8Array(Buffer.from(formatPriority(p), 'latin1'));
|
|
1647
|
+
var idv = writeVarInt(Number(streamId));
|
|
1648
|
+
var payload = new Uint8Array(idv.byteLength + field.byteLength);
|
|
1649
|
+
payload.set(idv, 0); payload.set(field, idv.byteLength);
|
|
1650
|
+
quic.sendStream(context.isServer ? 3 : 2, buildH3Frames([{ frame_type: PRIORITY_UPDATE_REQUEST, payload: payload }]), false);
|
|
1651
|
+
return true;
|
|
780
1652
|
}
|
|
781
1653
|
|
|
782
1654
|
function processQpackEncoderStream(s) {
|
|
783
|
-
|
|
1655
|
+
// An instruction cut short said how many bytes it needs; until they are
|
|
1656
|
+
// here, there is nothing to parse.
|
|
1657
|
+
if (s.buffered === 0 || (s.need > 0 && s.buffered < s.need)) return;
|
|
1658
|
+
var ext;
|
|
1659
|
+
try {
|
|
1660
|
+
// Only the unparsed bytes are copied (once per completed instruction,
|
|
1661
|
+
// given `need`), and no literal larger than our table can hold is
|
|
1662
|
+
// waited for.
|
|
1663
|
+
ext = parseQpackEncoderInstructions(copyBytes(s, s.buffered),
|
|
1664
|
+
context.local_qpack_max_table_capacity);
|
|
1665
|
+
} catch (err) {
|
|
1666
|
+
// RFC 9204 §4.2: a malformed encoder stream is QPACK_ENCODER_STREAM_ERROR,
|
|
1667
|
+
// not a decompression failure — nothing was being decompressed yet.
|
|
1668
|
+
err.h3ErrorCode = H3_ERR.QPACK_ENCODER_STREAM_ERROR;
|
|
1669
|
+
throw err;
|
|
1670
|
+
}
|
|
1671
|
+
consumeBytes(s, ext.consumed);
|
|
1672
|
+
s.need = ext.need;
|
|
784
1673
|
if (ext.instructions.length === 0) return;
|
|
785
|
-
s.from_offset = ext.new_from_offset;
|
|
786
1674
|
|
|
787
|
-
|
|
1675
|
+
// Each instruction is applied before the next is read. They used to be
|
|
1676
|
+
// collected and inserted together, so an instruction naming an entry
|
|
1677
|
+
// inserted earlier in the same batch found the wrong one. And one that
|
|
1678
|
+
// could not be resolved was skipped — leaving our insert count one short
|
|
1679
|
+
// of the peer's, and every later reference pointing at the wrong entry.
|
|
1680
|
+
// Both are errors the peer must hear about, not things to paper over.
|
|
788
1681
|
for (var i = 0; i < ext.instructions.length; i++) {
|
|
789
1682
|
var inst = ext.instructions[i];
|
|
1683
|
+
|
|
790
1684
|
if (inst.type === 'set_capacity') {
|
|
791
|
-
|
|
1685
|
+
if (inst.capacity > context.local_qpack_max_table_capacity) {
|
|
1686
|
+
throw qpackEncoderStreamError('capacity ' + inst.capacity + ' exceeds the ' +
|
|
1687
|
+
context.local_qpack_max_table_capacity + ' we advertised');
|
|
1688
|
+
}
|
|
1689
|
+
context.remote_qpack_table_capacity = inst.capacity;
|
|
1690
|
+
evictQpackTable();
|
|
792
1691
|
|
|
793
1692
|
} else if (inst.type === 'insert_name_ref') {
|
|
794
|
-
var name
|
|
795
|
-
if (inst.from_static
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
var di = context.remote_qpack_table_base_index - 1 - inst.name_index;
|
|
799
|
-
if (di >= 0 && di < context.remote_qpack_dynamic_table.length) {
|
|
800
|
-
name = context.remote_qpack_dynamic_table[di][0];
|
|
1693
|
+
var name;
|
|
1694
|
+
if (inst.from_static) {
|
|
1695
|
+
if (inst.name_index >= QPACK_STATIC.length) {
|
|
1696
|
+
throw qpackEncoderStreamError('static name index ' + inst.name_index + ' out of range');
|
|
801
1697
|
}
|
|
1698
|
+
name = QPACK_STATIC[inst.name_index][0];
|
|
1699
|
+
} else {
|
|
1700
|
+
name = remoteEntryByRelative(inst.name_index)[0];
|
|
802
1701
|
}
|
|
803
|
-
|
|
1702
|
+
remoteTableInsert(name, inst.value);
|
|
804
1703
|
|
|
805
1704
|
} else if (inst.type === 'insert_literal') {
|
|
806
|
-
|
|
1705
|
+
remoteTableInsert(inst.name, inst.value);
|
|
1706
|
+
|
|
1707
|
+
} else if (inst.type === 'duplicate') {
|
|
1708
|
+
var dup = remoteEntryByRelative(inst.index);
|
|
1709
|
+
remoteTableInsert(dup[0], dup[1]);
|
|
807
1710
|
}
|
|
808
1711
|
}
|
|
809
1712
|
|
|
810
|
-
|
|
811
|
-
|
|
1713
|
+
flushQpackAcks();
|
|
1714
|
+
unblockStreams();
|
|
1715
|
+
}
|
|
1716
|
+
|
|
1717
|
+
// ---- Blocked streams (RFC 9204 §2.1.2) ----
|
|
1718
|
+
//
|
|
1719
|
+
// A HEADERS frame whose section needs inserts not yet received waits — its
|
|
1720
|
+
// bytes stay in the stream's buffer, uncredited, and nothing behind it on
|
|
1721
|
+
// the stream is read — until the encoder stream brings them. At most
|
|
1722
|
+
// QPACK_BLOCKED_STREAMS at once; one more is the peer breaking the limit
|
|
1723
|
+
// we set: QPACK_DECOMPRESSION_FAILED (§2.2.1).
|
|
1724
|
+
var _blocked = new Map(); // stream id → the Required Insert Count it waits for
|
|
1725
|
+
|
|
1726
|
+
// The Required Insert Count of a field section, from its prefix.
|
|
1727
|
+
function sectionRequiredInsertCount(payload) {
|
|
1728
|
+
var ricEnc = readPrefixedInt(payload, 8, 0);
|
|
1729
|
+
if (!ricEnc) return 0;
|
|
1730
|
+
return decodeRequiredInsertCount(ricEnc.value, Math.floor(context.local_qpack_max_table_capacity / 32), context.remote_qpack_table_base_index);
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
// Whether stream `sid` must wait for this section; registers it if so.
|
|
1734
|
+
function sectionBlocked(sid, payload) {
|
|
1735
|
+
var ric;
|
|
1736
|
+
try { ric = sectionRequiredInsertCount(payload); }
|
|
1737
|
+
catch (e) { return false; } // malformed: decodeHeaders reports it
|
|
1738
|
+
if (ric <= context.remote_qpack_table_base_index) { _blocked.delete(sid); return false; }
|
|
1739
|
+
if (!_blocked.has(sid) && _blocked.size >= QPACK_BLOCKED_STREAMS) {
|
|
1740
|
+
throw h3Error(H3_ERR.QPACK_DECOMPRESSION_FAILED, 'more than ' + QPACK_BLOCKED_STREAMS + ' blocked streams');
|
|
1741
|
+
}
|
|
1742
|
+
_blocked.set(sid, ric);
|
|
1743
|
+
return true;
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// Inserts arrived: every stream they unblock is read on, in turn.
|
|
1747
|
+
function unblockStreams() {
|
|
1748
|
+
if (_blocked.size === 0) return;
|
|
1749
|
+
var ready = [];
|
|
1750
|
+
_blocked.forEach(function (ric, sid) { if (ric <= context.remote_qpack_table_base_index) ready.push(sid); });
|
|
1751
|
+
for (var i = 0; i < ready.length; i++) {
|
|
1752
|
+
_blocked.delete(ready[i]);
|
|
1753
|
+
var bs = context.streams[ready[i]];
|
|
1754
|
+
if (bs && bs.type === 'bidi') processBidiStream(ready[i], bs);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
|
|
1759
|
+
/**
|
|
1760
|
+
* Decode a QPACK header block, re-tagging any failure as
|
|
1761
|
+
* QPACK_DECOMPRESSION_FAILED (RFC 9204 §2.2.3) rather than the generic
|
|
1762
|
+
* framing error. The distinction matters to whoever reads the peer's logs:
|
|
1763
|
+
* a decompression failure says the two dynamic tables have diverged, which
|
|
1764
|
+
* is a different bug from a malformed frame.
|
|
1765
|
+
*/
|
|
1766
|
+
function decodeHeaders(payload, streamId) {
|
|
1767
|
+
var headers;
|
|
1768
|
+
try {
|
|
1769
|
+
headers = parseQpackHeaderBlock(
|
|
1770
|
+
payload,
|
|
1771
|
+
context.remote_qpack_dynamic_table,
|
|
1772
|
+
context.remote_qpack_table_base_index,
|
|
1773
|
+
// MaxEntries from the capacity WE advertised (§4.5.1.1).
|
|
1774
|
+
Math.floor(context.local_qpack_max_table_capacity / 32),
|
|
1775
|
+
context.local_max_header_size
|
|
1776
|
+
);
|
|
1777
|
+
} catch (err) {
|
|
1778
|
+
// Too large is this message's fault, not the tables': the stream goes,
|
|
1779
|
+
// the connection stays (see the caller).
|
|
1780
|
+
if (!err.excessive && !isInternalError(err)) err.h3ErrorCode = H3_ERR.QPACK_DECOMPRESSION_FAILED;
|
|
1781
|
+
throw err;
|
|
1782
|
+
}
|
|
1783
|
+
// §4.4.1: a section that used the dynamic table is acknowledged, so the
|
|
1784
|
+
// peer's encoder knows those references are done with.
|
|
1785
|
+
// Not for a stream we have abandoned: abortStream sent a Stream
|
|
1786
|
+
// Cancellation for it, and an acknowledgement after that names a section
|
|
1787
|
+
// the encoder no longer has — QPACK_DECODER_STREAM_ERROR on its side.
|
|
1788
|
+
if (headers._ric > 0 && decoderStreamId !== null && streamId !== undefined &&
|
|
1789
|
+
!_abortedHere.has(Number(streamId))) {
|
|
1790
|
+
quic.sendStream(decoderStreamId, new Uint8Array(qpackEncodeSectionAck(streamId)), false);
|
|
1791
|
+
}
|
|
1792
|
+
return headers;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
|
|
1796
|
+
// ---- Reading a request stream ----
|
|
1797
|
+
//
|
|
1798
|
+
// A request stream is a sequence of frames — HEADERS, DATA, perhaps trailing
|
|
1799
|
+
// HEADERS — and the bytes arrive in whatever pieces QUIC delivers, about one
|
|
1800
|
+
// packet each, cut with no regard for where a frame begins or ends.
|
|
1801
|
+
//
|
|
1802
|
+
// HEADERS is decoded whole, so it waits until all of it is here. DATA is not:
|
|
1803
|
+
// its bytes are handed to the application as they arrive, and s.data_left
|
|
1804
|
+
// counts down what is still to come of the frame. It used to wait for the
|
|
1805
|
+
// whole frame like HEADERS, re-joining everything received so far on every
|
|
1806
|
+
// packet — a 10MB DATA frame took some 20 seconds of CPU to parse, while the
|
|
1807
|
+
// application received nothing and the whole body sat in memory. One DATA
|
|
1808
|
+
// frame per write is exactly what h3_client sends.
|
|
1809
|
+
//
|
|
1810
|
+
// Frames of any other type carry nothing for a request stream; their bytes
|
|
1811
|
+
// are skipped the same way (s.skip_left), not buffered. They were ignored
|
|
1812
|
+
// before too, once complete.
|
|
1813
|
+
|
|
1814
|
+
// Bytes buffered on stream `s` past its read position.
|
|
1815
|
+
function bufferedBytes(s) {
|
|
1816
|
+
return s.buffered; // kept current by the push and consumeBytes — O(1)
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
// Move the read position on by `n` bytes, dropping chunks read to the end.
|
|
1820
|
+
// A body chunk (isBody) is credited by the caller; anything else consumed
|
|
1821
|
+
// here is credited at once.
|
|
1822
|
+
function consumeBytes(s, n, isBody) {
|
|
1823
|
+
if (!isBody && s.sid !== undefined) creditRecv(s.sid, n);
|
|
1824
|
+
s.from_offset += n;
|
|
1825
|
+
s.buffered -= n;
|
|
1826
|
+
while (s.chunks.length > 0 && s.from_offset >= s.chunks[0].length) {
|
|
1827
|
+
s.from_offset -= s.chunks[0].length;
|
|
1828
|
+
s.chunks.shift();
|
|
812
1829
|
}
|
|
813
1830
|
}
|
|
814
1831
|
|
|
1832
|
+
// Copy `n` bytes from the read position; the caller has checked they are here.
|
|
1833
|
+
function copyBytes(s, n) {
|
|
1834
|
+
var out = new Uint8Array(n);
|
|
1835
|
+
var got = 0, skip = s.from_offset;
|
|
1836
|
+
for (var i = 0; got < n; i++) {
|
|
1837
|
+
var c = s.chunks[i];
|
|
1838
|
+
if (skip >= c.length) { skip -= c.length; continue; }
|
|
1839
|
+
var take = Math.min(c.length - skip, n - got);
|
|
1840
|
+
out.set(c.subarray(skip, skip + take), got);
|
|
1841
|
+
got += take;
|
|
1842
|
+
skip = 0;
|
|
1843
|
+
}
|
|
1844
|
+
return out;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// The bytes from the read position to the end of the chunk holding it, at
|
|
1848
|
+
// most `max` — a view, not a copy. null when nothing is buffered.
|
|
1849
|
+
function nextPiece(s, max) {
|
|
1850
|
+
var skip = s.from_offset;
|
|
1851
|
+
for (var i = 0; i < s.chunks.length; i++) {
|
|
1852
|
+
var c = s.chunks[i];
|
|
1853
|
+
if (skip >= c.length) { skip -= c.length; continue; }
|
|
1854
|
+
return c.subarray(skip, skip + Math.min(c.length - skip, max));
|
|
1855
|
+
}
|
|
1856
|
+
return null;
|
|
1857
|
+
}
|
|
1858
|
+
|
|
815
1859
|
function processBidiStream(streamId, s) {
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1860
|
+
while (true) {
|
|
1861
|
+
// Inside a DATA frame: hand over whatever of it is here.
|
|
1862
|
+
if (s.data_left > 0) {
|
|
1863
|
+
var piece = nextPiece(s, s.data_left);
|
|
1864
|
+
if (!piece) break;
|
|
1865
|
+
consumeBytes(s, piece.length, true);
|
|
1866
|
+
if (!_bodyHeld.has(Number(streamId))) creditRecv(Number(streamId), piece.length);
|
|
1867
|
+
s.data_left -= piece.length;
|
|
1868
|
+
if (s.content_length !== null && s.content_length !== undefined) {
|
|
1869
|
+
s.body_received += piece.length;
|
|
1870
|
+
if (s.body_received > s.content_length) {
|
|
1871
|
+
streamError(Number(streamId), H3_ERR.H3_MESSAGE_ERROR, 'body longer than content-length');
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
ev.emit('http_body', Number(streamId), piece);
|
|
1876
|
+
if (context.streams[streamId] !== s) return;
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
|
|
1880
|
+
// Inside a frame of another type: skip whatever of it is here.
|
|
1881
|
+
if (s.skip_left > 0) {
|
|
1882
|
+
var skipNow = Math.min(s.skip_left, bufferedBytes(s));
|
|
1883
|
+
if (skipNow === 0) break;
|
|
1884
|
+
consumeBytes(s, skipNow);
|
|
1885
|
+
s.skip_left -= skipNow;
|
|
1886
|
+
continue;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// The next frame header: type and length, two varints of at most 8
|
|
1890
|
+
// bytes each.
|
|
1891
|
+
var avail = bufferedBytes(s);
|
|
1892
|
+
if (avail === 0) break;
|
|
1893
|
+
var head = copyBytes(s, Math.min(avail, 16));
|
|
1894
|
+
var ft = readVarInt(head, 0);
|
|
1895
|
+
if (!ft) break;
|
|
1896
|
+
var len = readVarInt(head, ft.byteLength);
|
|
1897
|
+
if (!len) break;
|
|
1898
|
+
var headLen = ft.byteLength + len.byteLength;
|
|
1899
|
+
|
|
1900
|
+
if (ft.value === 0) {
|
|
1901
|
+
// DATA
|
|
1902
|
+
// RFC 9114 §4.1: a message starts with HEADERS; DATA before it is an
|
|
1903
|
+
// invalid frame sequence — H3_FRAME_UNEXPECTED, a connection error.
|
|
1904
|
+
// It used to be delivered, and h3_server built a request around it:
|
|
1905
|
+
// empty method and URL, handler called, and an entry nothing would
|
|
1906
|
+
// ever remove.
|
|
1907
|
+
if (!s.headers_seen) {
|
|
1908
|
+
throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'DATA before HEADERS on stream ' + streamId);
|
|
1909
|
+
}
|
|
1910
|
+
// Nor after the trailers, which end the message (§4.1): taken as more
|
|
1911
|
+
// body, the message read here was not the one framed — what a proxy
|
|
1912
|
+
// in front would have passed on.
|
|
1913
|
+
if (s.trailers_seen) {
|
|
1914
|
+
throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'DATA after trailers on stream ' + streamId);
|
|
835
1915
|
}
|
|
1916
|
+
consumeBytes(s, headLen);
|
|
1917
|
+
s.data_left = len.value;
|
|
1918
|
+
if (len.value === 0) {
|
|
1919
|
+
// An empty one is still reported, as it always was.
|
|
1920
|
+
ev.emit('http_body', Number(streamId), new Uint8Array(0));
|
|
1921
|
+
if (context.streams[streamId] !== s) return;
|
|
1922
|
+
}
|
|
1923
|
+
continue;
|
|
1924
|
+
}
|
|
1925
|
+
// Frames of the control stream, and HTTP/2's reserved ones, are
|
|
1926
|
+
// H3_FRAME_UNEXPECTED on a request stream (§7.2.3, §7.2.4, §7.2.6,
|
|
1927
|
+
// §7.2.7, §7.2.8; RFC 9218 §7.2 for PRIORITY_UPDATE).
|
|
1928
|
+
if (ft.value === 0x03 || ft.value === 0x04 || ft.value === 0x07 || ft.value === 0x0d ||
|
|
1929
|
+
ft.value === PRIORITY_UPDATE_REQUEST || ft.value === PRIORITY_UPDATE_PUSH || reservedH2Frame(ft.value)) {
|
|
1930
|
+
throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'frame type 0x' + ft.value.toString(16) + ' on request stream ' + streamId);
|
|
836
1931
|
}
|
|
1932
|
+
// PUSH_PROMISE (§7.2.5): from a client, H3_FRAME_UNEXPECTED; at a
|
|
1933
|
+
// client, a push ID above the maximum it allowed — none — H3_ID_ERROR.
|
|
1934
|
+
if (ft.value === 0x05) {
|
|
1935
|
+
if (context.isServer) throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'PUSH_PROMISE from the client on stream ' + streamId);
|
|
1936
|
+
throw h3Error(H3_ERR.H3_ID_ERROR, 'PUSH_PROMISE on stream ' + streamId + ', no push allowed');
|
|
1937
|
+
}
|
|
1938
|
+
if (ft.value !== 1) {
|
|
1939
|
+
consumeBytes(s, headLen);
|
|
1940
|
+
s.skip_left = len.value;
|
|
1941
|
+
continue;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// HEADERS — whole, or wait for the rest. Waiting is bounded: QUIC
|
|
1945
|
+
// returns flow-control credit as it hands bytes up, so it does not stop
|
|
1946
|
+
// a peer from announcing a frame of any length and sending it all. One
|
|
1947
|
+
// larger than the field section size we advertised (SETTINGS_MAX_
|
|
1948
|
+
// FIELD_SECTION_SIZE) is refused on sight, before it is buffered. The
|
|
1949
|
+
// encoded size is the right thing to compare: literals count their
|
|
1950
|
+
// full length plus 32 bytes per field there, more than they take here.
|
|
1951
|
+
// A field section after the trailers: §4.1, H3_FRAME_UNEXPECTED.
|
|
1952
|
+
if (s.trailers_seen) {
|
|
1953
|
+
throw h3Error(H3_ERR.H3_FRAME_UNEXPECTED, 'HEADERS after trailers on stream ' + streamId);
|
|
1954
|
+
}
|
|
1955
|
+
if (len.value > context.local_max_header_size) {
|
|
1956
|
+
streamError(Number(streamId), H3_ERR.H3_EXCESSIVE_LOAD,
|
|
1957
|
+
'HEADERS of ' + len.value + ' bytes exceeds ' + context.local_max_header_size);
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
if (avail < headLen + len.value) break;
|
|
1961
|
+
// Waits for the encoder stream when it must (see sectionBlocked): the
|
|
1962
|
+
// frame stays in the buffer, read again once unblocked.
|
|
1963
|
+
// Already waiting, and the inserts it needs are not here yet: nothing
|
|
1964
|
+
// to do. The frame was copied and its prefix decoded again on every
|
|
1965
|
+
// packet that arrived on the stream meanwhile.
|
|
1966
|
+
if (_blocked.has(Number(streamId)) &&
|
|
1967
|
+
_blocked.get(Number(streamId)) > context.remote_qpack_table_base_index) return;
|
|
1968
|
+
var whole = copyBytes(s, headLen + len.value);
|
|
1969
|
+
var payload = whole.subarray(headLen);
|
|
1970
|
+
if (sectionBlocked(Number(streamId), payload)) return;
|
|
1971
|
+
consumeBytes(s, headLen + len.value);
|
|
1972
|
+
s.headers_seen = true;
|
|
1973
|
+
|
|
1974
|
+
// HEADERS
|
|
1975
|
+
|
|
1976
|
+
var headers;
|
|
1977
|
+
try { headers = decodeHeaders(payload, Number(streamId)); }
|
|
1978
|
+
catch (e) {
|
|
1979
|
+
if (!e.excessive) throw e;
|
|
1980
|
+
streamError(Number(streamId), H3_ERR.H3_EXCESSIVE_LOAD, e.message);
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
// Refused in 'stream_open' (abortStream forgot the stream): stop
|
|
1985
|
+
// here, before the request exists. The block is still decoded — the
|
|
1986
|
+
// QPACK state is shared with the peer and has to see every section —
|
|
1987
|
+
// but nothing above this layer hears of it. The comment above
|
|
1988
|
+
// promised exactly this; the code went on to emit http_headers, and
|
|
1989
|
+
// a refused request reached its handler all the same.
|
|
1990
|
+
if (context.streams[streamId] !== s) return;
|
|
1991
|
+
|
|
1992
|
+
// The section must be well formed (RFC 9114 §4.1.2, §4.2, §4.3), by the
|
|
1993
|
+
// rules HTTP/2 uses too (validateFieldSection). None were checked here:
|
|
1994
|
+
// a value with CR LF, a connection-specific field, a request without
|
|
1995
|
+
// :path reached the handler as it came. Malformed: H3_MESSAGE_ERROR on
|
|
1996
|
+
// this stream. A section after the final head is trailers; a 1xx
|
|
1997
|
+
// response (103 Early Hints) is not the final head.
|
|
1998
|
+
var isTrailers = !!s.final_head_seen;
|
|
1999
|
+
var r;
|
|
2000
|
+
try {
|
|
2001
|
+
r = validateFieldSection(headers._list || [], {
|
|
2002
|
+
isServer: context.isServer,
|
|
2003
|
+
trailers: isTrailers,
|
|
2004
|
+
connectProtocol: !!context.local_datagram_support
|
|
2005
|
+
});
|
|
2006
|
+
} catch (e) {
|
|
2007
|
+
streamError(Number(streamId), H3_ERR.H3_MESSAGE_ERROR, e.message);
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
if (!isTrailers && !(!context.isServer && /^1/.test(r.headers[':status']))) s.final_head_seen = true;
|
|
2011
|
+
if (isTrailers) s.trailers_seen = true;
|
|
2012
|
+
|
|
2013
|
+
// content-length against the body that actually comes (§4.1.2) — for
|
|
2014
|
+
// requests: a response to HEAD, or a 304, carries a length and no body.
|
|
2015
|
+
if (context.isServer) {
|
|
2016
|
+
if (!isTrailers) {
|
|
2017
|
+
try { s.content_length = parseContentLength(r.headers['content-length']); }
|
|
2018
|
+
catch (e) { streamError(Number(streamId), H3_ERR.H3_MESSAGE_ERROR, e.message); return; }
|
|
2019
|
+
s.body_received = 0;
|
|
2020
|
+
} else if (s.content_length !== null && s.content_length !== undefined && s.body_received !== s.content_length) {
|
|
2021
|
+
streamError(Number(streamId), H3_ERR.H3_MESSAGE_ERROR, 'body shorter than content-length');
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// The pseudo-header order, for the protocol fingerprint — once the
|
|
2027
|
+
// section is known to be well formed. Emitted separately because by the
|
|
2028
|
+
// time the headers reach an application they are an object and the
|
|
2029
|
+
// order is gone; this decode is the only place that sees the sequence.
|
|
2030
|
+
if (r.pseudoOrder.length) ev.emit('pseudo_order', r.pseudoOrder);
|
|
2031
|
+
if (context.isServer && !isTrailers) applyRequestPriority(Number(streamId), r.headers);
|
|
2032
|
+
if (context.isServer && !isTrailers && context.local_webtransport &&
|
|
2033
|
+
r.headers[':method'] === 'CONNECT' && r.headers[':protocol'] === 'webtransport') {
|
|
2034
|
+
// One session more than we said the peer may have: refused before
|
|
2035
|
+
// the application hears of it — H3_REQUEST_REJECTED, so it may try
|
|
2036
|
+
// again later, or on another connection.
|
|
2037
|
+
if (context.local_wt_max_sessions !== null && _wtSessions.size + _wtConnects.size >= context.local_wt_max_sessions) {
|
|
2038
|
+
streamError(Number(streamId), H3_ERR.H3_REQUEST_REJECTED, 'more WebTransport sessions than SETTINGS_WT_MAX_SESSIONS');
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
_wtConnects.add(Number(streamId));
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
// Trailers (a field section after the final head, RFC 9114 §4.1) are
|
|
2045
|
+
// their own event. As 'http_headers' they reached the layers above as a
|
|
2046
|
+
// second head: the client took them for the response's headers — its
|
|
2047
|
+
// status read 0 and 'response' fired twice — and the server merged
|
|
2048
|
+
// them into the request's headers.
|
|
2049
|
+
ev.emit(isTrailers ? 'http_trailers' : 'http_headers', Number(streamId), r.headers);
|
|
2050
|
+
// A handler may cancel the stream the moment it sees the headers;
|
|
2051
|
+
// DATA behind them in this batch must not surface as a body for a
|
|
2052
|
+
// request that no longer exists — h3_server would build a new one.
|
|
2053
|
+
if (context.streams[streamId] !== s) return;
|
|
837
2054
|
}
|
|
838
2055
|
|
|
839
2056
|
// FIN received → stream complete (emitted only once)
|
|
840
2057
|
if (s.fin_received && !s.http_end_emitted) {
|
|
2058
|
+
// Every byte of the stream is here once its FIN is: anything left
|
|
2059
|
+
// unread is a frame cut short by the end of the stream. RFC 9114 §7.1:
|
|
2060
|
+
// H3_FRAME_ERROR. Ending the request quietly instead would hand the
|
|
2061
|
+
// application a truncated body as a complete one — the more so now that
|
|
2062
|
+
// DATA is delivered as it arrives.
|
|
2063
|
+
if (s.data_left > 0 || s.skip_left > 0 || bufferedBytes(s) > 0) {
|
|
2064
|
+
var truncated = new Error('stream ' + streamId + ' ended inside a frame');
|
|
2065
|
+
truncated.h3ErrorCode = H3_ERR.H3_FRAME_ERROR;
|
|
2066
|
+
throw truncated;
|
|
2067
|
+
}
|
|
2068
|
+
// A stream that ended without ever carrying HEADERS holds no message:
|
|
2069
|
+
// RFC 9114 §4.1.2, H3_REQUEST_INCOMPLETE. Reporting its end made the
|
|
2070
|
+
// layer above build a request out of nothing.
|
|
2071
|
+
if (!s.headers_seen) {
|
|
2072
|
+
streamError(Number(streamId), H3_ERR.H3_REQUEST_INCOMPLETE, 'stream ended without HEADERS');
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
if (s.content_length !== null && s.content_length !== undefined && s.body_received !== s.content_length) {
|
|
2076
|
+
streamError(Number(streamId), H3_ERR.H3_MESSAGE_ERROR, 'body shorter than content-length');
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
841
2079
|
s.http_end_emitted = true;
|
|
842
2080
|
ev.emit('http_end', Number(streamId));
|
|
2081
|
+
// Its receiving side is over, and nothing more can arrive on it:
|
|
2082
|
+
// quic_connection discards every frame after the FIN (recv_final). Kept,
|
|
2083
|
+
// the entry held every byte the stream ever carried — a whole upload —
|
|
2084
|
+
// until the connection closed. Only this side is done; responses are
|
|
2085
|
+
// sent without this entry.
|
|
2086
|
+
// If it was a WebTransport CONNECT stream, its FIN ended the session:
|
|
2087
|
+
// a later stream naming it belongs to nothing, and the streams still
|
|
2088
|
+
// open in it are closed with it (releaseStream → endWtSession).
|
|
2089
|
+
if (context.streams[streamId] === s) releaseStream(Number(streamId), true);
|
|
843
2090
|
}
|
|
844
2091
|
}
|
|
845
2092
|
|
|
846
2093
|
|
|
847
2094
|
// ---- Send methods ----
|
|
848
2095
|
|
|
2096
|
+
// One encoder whatever the peer allows. With no dynamic table — the peer
|
|
2097
|
+
// advertised capacity 0, or its SETTINGS have not arrived yet, and
|
|
2098
|
+
// encoderTable's capacity is still 0 — it uses the static table and Huffman
|
|
2099
|
+
// alone, and references and inserts nothing. (A second,
|
|
2100
|
+
// static-only encoder used to serve that case, without Huffman: a typical
|
|
2101
|
+
// page's response headers came out 313 bytes instead of 245.)
|
|
849
2102
|
function sendHeaders(streamId, headers, fin) {
|
|
850
|
-
var
|
|
851
|
-
|
|
2103
|
+
var list = [];
|
|
2104
|
+
for (var hname in headers) {
|
|
2105
|
+
if (Object.prototype.hasOwnProperty.call(headers, hname)) {
|
|
2106
|
+
// A list: one field line per value (Set-Cookie above all, which may
|
|
2107
|
+
// not be combined), not String(list).
|
|
2108
|
+
var hv = headers[hname];
|
|
2109
|
+
if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) list.push([hname, String(hv[hi])]); }
|
|
2110
|
+
else list.push([hname, hv]);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
var enc = qpackEncodeHeaderBlock(list, encoderTable, {
|
|
2114
|
+
knownReceivedCount: qpackKnownReceived,
|
|
2115
|
+
history: encoderHistory,
|
|
2116
|
+
// The Required Insert Count is encoded against the capacity the PEER
|
|
2117
|
+
// advertised, not the smaller one this encoder uses (§4.5.1.1).
|
|
2118
|
+
peerMaxTableCapacity: context.remote_qpack_max_table_capacity || 0,
|
|
2119
|
+
// authorization and cookie carry secrets, and compressed size is
|
|
2120
|
+
// observable — the CRIME attack. They are never inserted.
|
|
2121
|
+
neverIndex: { 'authorization': true, 'cookie': true,
|
|
2122
|
+
'proxy-authorization': true, 'set-cookie': true }
|
|
2123
|
+
});
|
|
2124
|
+
// Insertions FIRST, on their own stream: they are for the next request,
|
|
2125
|
+
// not this one.
|
|
2126
|
+
flushQpackInserts(enc.inserts);
|
|
2127
|
+
|
|
2128
|
+
var frame = buildH3Frames([{ frame_type: 1, payload: new Uint8Array(enc.block) }]);
|
|
852
2129
|
if (DEBUG) console.log('[h3] sendHeaders stream=' + streamId + ' len=' + frame.byteLength + ' fin=' + !!fin);
|
|
853
2130
|
quic.sendStream(streamId, frame, !!fin);
|
|
854
2131
|
}
|
|
@@ -861,14 +2138,24 @@ function H3Connection(options) {
|
|
|
861
2138
|
return;
|
|
862
2139
|
}
|
|
863
2140
|
if (typeof data === 'string') data = new TextEncoder().encode(data);
|
|
864
|
-
|
|
865
|
-
|
|
2141
|
+
// The DATA frame header and the payload go down as two pieces: the payload
|
|
2142
|
+
// is queued by reference rather than copied behind a few header bytes.
|
|
2143
|
+
var header = concatUint8Arrays([writeVarInt(0), writeVarInt(data.byteLength)]);
|
|
2144
|
+
quic.sendStream(streamId, [header, data], !!fin);
|
|
866
2145
|
}
|
|
867
2146
|
|
|
868
2147
|
|
|
869
2148
|
// ---- Public API ----
|
|
870
2149
|
|
|
2150
|
+
useRememberedSettings();
|
|
2151
|
+
|
|
871
2152
|
var api = {
|
|
2153
|
+
/** Stop listening to the QUIC connection: this HTTP/3 layer is done
|
|
2154
|
+
* with it, another may take over (a client's refused 0-RTT). */
|
|
2155
|
+
detach: function () {
|
|
2156
|
+
for (var i = 0; i < _quicListeners.length; i++) if (quic.off) quic.off(_quicListeners[i][0], _quicListeners[i][1]);
|
|
2157
|
+
_quicListeners = [];
|
|
2158
|
+
},
|
|
872
2159
|
context: context,
|
|
873
2160
|
|
|
874
2161
|
on: function (name, fn) { ev.on(name, fn); },
|
|
@@ -876,35 +2163,112 @@ function H3Connection(options) {
|
|
|
876
2163
|
|
|
877
2164
|
sendHeaders: sendHeaders,
|
|
878
2165
|
sendBody: sendBody,
|
|
2166
|
+
abortStream: abortStream,
|
|
879
2167
|
sendControlStreams: sendControlStreams,
|
|
2168
|
+
sendPriorityUpdate: sendPriorityUpdate,
|
|
880
2169
|
sendGoaway: sendGoaway,
|
|
881
|
-
claimStream: function (streamId) { _claimedStreams.add(Number(streamId)); },
|
|
882
2170
|
|
|
883
2171
|
// WebTransport server-side API
|
|
884
2172
|
registerWebTransportSession: function (sessionStreamId) {
|
|
885
|
-
|
|
2173
|
+
var id = Number(sessionStreamId);
|
|
2174
|
+
_wtSessions.add(id);
|
|
2175
|
+
_wtConnects.delete(id);
|
|
2176
|
+
settleHeldWt(id, true); // its streams that came early
|
|
886
2177
|
},
|
|
887
2178
|
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
2179
|
+
// The application answered a WebTransport CONNECT with something other
|
|
2180
|
+
// than 2xx: streams that came early for it are refused.
|
|
2181
|
+
rejectWebTransportSession: function (sessionStreamId) {
|
|
2182
|
+
var id = Number(sessionStreamId);
|
|
2183
|
+
_wtConnects.delete(id);
|
|
2184
|
+
settleHeldWt(id, false);
|
|
2185
|
+
},
|
|
2186
|
+
|
|
2187
|
+
// We closed it (CLOSE_WEBTRANSPORT_SESSION): its streams go now, reset
|
|
2188
|
+
// with WEBTRANSPORT_SESSION_GONE as when the peer ends it.
|
|
2189
|
+
endWebTransportSession: function (sessionStreamId) { endWtSession(Number(sessionStreamId)); },
|
|
2190
|
+
|
|
2191
|
+
/**
|
|
2192
|
+
* Take a stream out of HTTP/3 framing and hand its bytes through raw.
|
|
2193
|
+
*
|
|
2194
|
+
* Called once an extended CONNECT has been accepted with a 2xx. From then
|
|
2195
|
+
* on the stream carries whatever protocol `:protocol` named — WebSocket
|
|
2196
|
+
* frames, for RFC 9220 — and 'tunnel_data' delivers them verbatim.
|
|
2197
|
+
*/
|
|
2198
|
+
registerTunnelStream: function (streamId) {
|
|
2199
|
+
var id = Number(streamId);
|
|
2200
|
+
_tunnelStreams.add(id);
|
|
2201
|
+
_tunnelPendingIds.delete(id);
|
|
2202
|
+
// Anything that arrived before the accept is delivered now — after the
|
|
2203
|
+
// caller's accept returns — rather than on the next arrival, which may
|
|
2204
|
+
// never come (the client's FIN may already be here).
|
|
2205
|
+
if (_tunnelPending[id] || _tunnelPendingFin.has(id)) {
|
|
2206
|
+
queueMicrotask(function () { if (_tunnelStreams.has(id)) replayTunnelPending(id); });
|
|
901
2207
|
}
|
|
2208
|
+
},
|
|
2209
|
+
|
|
2210
|
+
/** Stop reading a stream without resetting what we sent on it. */
|
|
2211
|
+
stopReading: stopReading,
|
|
2212
|
+
|
|
2213
|
+
/** A WebTransport stream we opened has ended on our side. */
|
|
2214
|
+
releaseWebTransportStream: function (streamId) { releaseStream(streamId); },
|
|
2215
|
+
|
|
2216
|
+
/**
|
|
2217
|
+
* Mark a stream as a tunnel-in-progress: stop feeding it to the HTTP/3
|
|
2218
|
+
* frame parser, but hold its bytes until registerTunnelStream() accepts.
|
|
2219
|
+
*
|
|
2220
|
+
* Called the moment `:protocol` is seen, which is before the application
|
|
2221
|
+
* has decided whether to accept. Without it, a client that sends its first
|
|
2222
|
+
* WebSocket frame immediately after the CONNECT has that frame parsed as
|
|
2223
|
+
* an HTTP/3 frame and lost.
|
|
2224
|
+
*/
|
|
2225
|
+
beginTunnelStream: function (streamId) {
|
|
2226
|
+
_tunnelPendingIds.add(Number(streamId));
|
|
2227
|
+
// Its bytes are the tunnel's, bounded by MAX_TUNNEL_PENDING until the
|
|
2228
|
+
// accept: credited as delivered from here on.
|
|
2229
|
+
_bodyHeld.delete(Number(streamId));
|
|
2230
|
+
releaseRecvCredit(Number(streamId));
|
|
2231
|
+
},
|
|
902
2232
|
|
|
903
|
-
|
|
2233
|
+
/**
|
|
2234
|
+
* The application returns a request body's flow-control credit itself, as
|
|
2235
|
+
* it reads (see _bodyHeld): from now on, `creditBody(id, n)` for each n
|
|
2236
|
+
* bytes read, and `releaseBody(id)` when it will read no more.
|
|
2237
|
+
*/
|
|
2238
|
+
holdBody: function (streamId) {
|
|
2239
|
+
var id = Number(streamId);
|
|
2240
|
+
if (context.streams[id]) _bodyHeld.add(id);
|
|
2241
|
+
},
|
|
2242
|
+
creditBody: function (streamId, n) { if (_bodyHeld.has(Number(streamId))) creditRecv(Number(streamId), n); },
|
|
2243
|
+
releaseBody: function (streamId) {
|
|
2244
|
+
var id = Number(streamId);
|
|
2245
|
+
if (!_bodyHeld.delete(id)) return;
|
|
2246
|
+
releaseRecvCredit(id);
|
|
2247
|
+
},
|
|
2248
|
+
|
|
2249
|
+
/** Write raw bytes onto a tunnel stream. */
|
|
2250
|
+
sendTunnelData: function (streamId, data, fin) {
|
|
2251
|
+
quic.sendStream(Number(streamId), data, !!fin);
|
|
2252
|
+
},
|
|
2253
|
+
|
|
2254
|
+
// A WebTransport stream of our own, on either side: the next free stream
|
|
2255
|
+
// number (quic's nextLocalStreamId), opened now with its prefix —
|
|
2256
|
+
// VarInt(0x41 or 0x54) + VarInt(session ID) — as a browser does.
|
|
2257
|
+
createWebTransportStream: function (sessionId, isBidi) {
|
|
2258
|
+
var streamId = quic.nextLocalStreamId(!!isBidi);
|
|
904
2259
|
quic.sendStream(streamId, wt_stream_header(isBidi, sessionId), false);
|
|
905
2260
|
|
|
906
|
-
|
|
907
|
-
|
|
2261
|
+
// Claimed for what the peer sends back on it — so only a bidirectional
|
|
2262
|
+
// one. Nothing ever arrives on our own unidirectional stream, so no FIN
|
|
2263
|
+
// ever released a claim made for it.
|
|
2264
|
+
if (isBidi) {
|
|
2265
|
+
_claimedStreams.add(streamId);
|
|
2266
|
+
_wtStreams[streamId] = { sessionId: sessionId, isBidi: isBidi };
|
|
2267
|
+
} else {
|
|
2268
|
+
// Tracked only so the session's end can reset it (released by the
|
|
2269
|
+
// writer at its end: releaseWebTransportStream).
|
|
2270
|
+
_wtLocalUni[streamId] = Number(sessionId);
|
|
2271
|
+
}
|
|
908
2272
|
|
|
909
2273
|
return streamId;
|
|
910
2274
|
}
|
|
@@ -917,4 +2281,106 @@ function H3Connection(options) {
|
|
|
917
2281
|
}
|
|
918
2282
|
|
|
919
2283
|
|
|
920
|
-
|
|
2284
|
+
// ============================================================
|
|
2285
|
+
// WebTransport stream error codes (draft-ietf-webtrans-http3 §4.4)
|
|
2286
|
+
// ============================================================
|
|
2287
|
+
//
|
|
2288
|
+
// A WebTransport application resets or stops a stream with a 32-bit code of
|
|
2289
|
+
// its own; on the wire it is an HTTP/3 error code, in a range reserved for
|
|
2290
|
+
// it, which skips the codes reserved for greasing (0x1f * N + 0x21). Sent
|
|
2291
|
+
// raw, a WebTransport code collided with HTTP/3's own (H3_NO_ERROR is
|
|
2292
|
+
// 0x100), and the browser read neither.
|
|
2293
|
+
var WT_ERR_FIRST = 0x52e4a40fa8db;
|
|
2294
|
+
var WT_ERR_LAST = 0x52e5ac983162;
|
|
2295
|
+
|
|
2296
|
+
function wtErrorToH3(code) {
|
|
2297
|
+
code = Number(code);
|
|
2298
|
+
if (!(code >= 0 && code <= 0xffffffff) || Math.floor(code) !== code) code = 0;
|
|
2299
|
+
return WT_ERR_FIRST + code + Math.floor(code / 0x1e);
|
|
2300
|
+
}
|
|
2301
|
+
|
|
2302
|
+
// The WebTransport code an HTTP/3 code carries; null if it carries none.
|
|
2303
|
+
function h3ErrorToWt(h3code) {
|
|
2304
|
+
h3code = Number(h3code);
|
|
2305
|
+
if (!(h3code >= WT_ERR_FIRST && h3code <= WT_ERR_LAST)) return null;
|
|
2306
|
+
if ((h3code - 0x21) % 0x1f === 0) return null; // a grease code
|
|
2307
|
+
var shifted = h3code - WT_ERR_FIRST;
|
|
2308
|
+
return shifted - Math.floor(shifted / 0x1f);
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2311
|
+
// ============================================================
|
|
2312
|
+
// WebTransport session capsules (RFC 9297; draft-ietf-webtrans-http3 §5)
|
|
2313
|
+
// ============================================================
|
|
2314
|
+
//
|
|
2315
|
+
// After the 2xx, a session's CONNECT stream carries capsules — type, length,
|
|
2316
|
+
// value — in HTTP/3 DATA frames. Two concern the session:
|
|
2317
|
+
// CLOSE_WEBTRANSPORT_SESSION (0x2843): a 32-bit application code and a
|
|
2318
|
+
// UTF-8 reason of up to 1024 bytes; the sender then ends the stream;
|
|
2319
|
+
// DRAIN_WEBTRANSPORT_SESSION (0x78ae): empty — finish up, no new work.
|
|
2320
|
+
// Any other type is skipped (RFC 9297 §3.2), without being buffered.
|
|
2321
|
+
var CAPSULE_CLOSE_WT = 0x2843;
|
|
2322
|
+
var CAPSULE_DRAIN_WT = 0x78ae;
|
|
2323
|
+
var WT_CLOSE_REASON_MAX = 1024;
|
|
2324
|
+
|
|
2325
|
+
function encodeCapsule(type, value) {
|
|
2326
|
+
return concatUint8Arrays([writeVarInt(type), writeVarInt(value.byteLength), value]);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
2329
|
+
function wtCloseCapsule(code, reason) {
|
|
2330
|
+
code = Number(code);
|
|
2331
|
+
if (!(code >= 0 && code <= 0xffffffff) || Math.floor(code) !== code) code = 0;
|
|
2332
|
+
var r = Buffer.from(String(reason == null ? '' : reason), 'utf8');
|
|
2333
|
+
if (r.length > WT_CLOSE_REASON_MAX) {
|
|
2334
|
+
// Cut on a character boundary: back off continuation bytes (10xxxxxx).
|
|
2335
|
+
var end = WT_CLOSE_REASON_MAX;
|
|
2336
|
+
while (end > 0 && (r[end] & 0xc0) === 0x80) end--;
|
|
2337
|
+
r = r.subarray(0, end);
|
|
2338
|
+
}
|
|
2339
|
+
var v = new Uint8Array(4 + r.length);
|
|
2340
|
+
v[0] = code >>> 24; v[1] = (code >>> 16) & 0xff; v[2] = (code >>> 8) & 0xff; v[3] = code & 0xff;
|
|
2341
|
+
v.set(r, 4);
|
|
2342
|
+
return encodeCapsule(CAPSULE_CLOSE_WT, v);
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
function wtDrainCapsule() { return encodeCapsule(CAPSULE_DRAIN_WT, new Uint8Array(0)); }
|
|
2346
|
+
|
|
2347
|
+
// Reads a CONNECT stream's capsules as they arrive, however they are split:
|
|
2348
|
+
// onClose({ closeCode, reason }) and onDrain(). A CLOSE whose value is
|
|
2349
|
+
// malformed (shorter than its code, or a reason past the limit) is still a
|
|
2350
|
+
// close — with code 0 and no reason.
|
|
2351
|
+
function createCapsuleReader(onClose, onDrain) {
|
|
2352
|
+
var buf = new Uint8Array(0), skip = 0;
|
|
2353
|
+
return function feed(data) {
|
|
2354
|
+
if (skip > 0) {
|
|
2355
|
+
var n = Math.min(skip, data.byteLength);
|
|
2356
|
+
skip -= n; data = data.subarray(n);
|
|
2357
|
+
}
|
|
2358
|
+
if (data.byteLength === 0) return;
|
|
2359
|
+
buf = buf.byteLength ? concatUint8Arrays([buf, data]) : data;
|
|
2360
|
+
for (;;) {
|
|
2361
|
+
var t = readVarInt(buf, 0); if (!t) return;
|
|
2362
|
+
var l = readVarInt(buf, t.byteLength); if (!l) return;
|
|
2363
|
+
var head = t.byteLength + l.byteLength;
|
|
2364
|
+
var known = t.value === CAPSULE_CLOSE_WT || t.value === CAPSULE_DRAIN_WT;
|
|
2365
|
+
if (!known || l.value > 4 + WT_CLOSE_REASON_MAX) {
|
|
2366
|
+
if (known) onClose({ closeCode: 0, reason: '' }); // oversized: a close all the same
|
|
2367
|
+
var have = buf.byteLength - head;
|
|
2368
|
+
if (have >= l.value) { buf = buf.subarray(head + l.value); continue; }
|
|
2369
|
+
skip = l.value - have; buf = new Uint8Array(0); return;
|
|
2370
|
+
}
|
|
2371
|
+
if (buf.byteLength < head + l.value) return; // the rest is still to come
|
|
2372
|
+
var v = buf.subarray(head, head + l.value);
|
|
2373
|
+
buf = buf.subarray(head + l.value);
|
|
2374
|
+
if (t.value === CAPSULE_DRAIN_WT) { onDrain(); continue; }
|
|
2375
|
+
if (v.byteLength < 4) { onClose({ closeCode: 0, reason: '' }); continue; }
|
|
2376
|
+
onClose({ closeCode: ((v[0] << 24) >>> 0) + (v[1] << 16) + (v[2] << 8) + v[3],
|
|
2377
|
+
reason: Buffer.from(v.subarray(4)).toString('utf8') });
|
|
2378
|
+
}
|
|
2379
|
+
};
|
|
2380
|
+
}
|
|
2381
|
+
|
|
2382
|
+
// parseQpackHeaderBlock is exported for the QPACK tests, which check it
|
|
2383
|
+
// against sections from an independent encoder.
|
|
2384
|
+
export { settingsContext as h3SettingsContext };
|
|
2385
|
+
export { H3Connection, H3_ERR, H3_ERR_NAME, parseQpackHeaderBlock, wtErrorToH3, h3ErrorToWt,
|
|
2386
|
+
wtCloseCapsule, wtDrainCapsule, createCapsuleReader };
|