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