quico 0.3.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 +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- 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 +257 -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 +476 -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 +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- 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/qpack.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
QPACK_STATIC,
|
|
17
|
+
buildStaticLookup,
|
|
18
|
+
encodeStringLiteral,
|
|
19
|
+
encodePrefixedInt,
|
|
20
|
+
readPrefixedInt,
|
|
21
|
+
entrySize,
|
|
22
|
+
ENTRY_OVERHEAD
|
|
23
|
+
} from './hpack_primitives.js';
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
// ============================================================
|
|
27
|
+
// QPACK dynamic table (RFC 9204).
|
|
28
|
+
//
|
|
29
|
+
// HPACK's table and this one solve the same problem and are not the same
|
|
30
|
+
// thing, because the transport underneath is different. In HTTP/2 both sides
|
|
31
|
+
// process header blocks in order, so a shared table stays in step for free.
|
|
32
|
+
// Over QUIC there is no order between streams: a request that references an
|
|
33
|
+
// entry can arrive before the instruction that created it.
|
|
34
|
+
//
|
|
35
|
+
// QPACK's answer is to move insertions onto their own unidirectional stream
|
|
36
|
+
// and to make every reference explicit about what it depends on. That is
|
|
37
|
+
// where the complexity comes from, and it is also where the risk comes from:
|
|
38
|
+
// a reference to an entry that has not arrived yet BLOCKS the decoder — the
|
|
39
|
+
// precise thing QUIC exists to avoid.
|
|
40
|
+
//
|
|
41
|
+
// THIS ENCODER TAKES NO SUCH RISK.
|
|
42
|
+
//
|
|
43
|
+
// RFC 9204 §2.1.2 names both strategies: an encoder may reference entries
|
|
44
|
+
// that are still in flight and compress better, or it may reference only
|
|
45
|
+
// entries the peer has acknowledged and never block. This one does the
|
|
46
|
+
// second. Concretely:
|
|
47
|
+
//
|
|
48
|
+
// - We advertise SETTINGS_QPACK_BLOCKED_STREAMS = 0, promising never to
|
|
49
|
+
// send a reference the peer cannot resolve immediately.
|
|
50
|
+
// - An entry becomes usable only after the peer acknowledges its insertion
|
|
51
|
+
// on the decoder stream.
|
|
52
|
+
// - Until then the field goes out as a literal, exactly as before.
|
|
53
|
+
//
|
|
54
|
+
// What that costs is the first request on a connection, which is no better
|
|
55
|
+
// than it is today. What it buys is every request after it: measured against
|
|
56
|
+
// a real Chrome header set, a subresource request drops from 412 bytes to 80
|
|
57
|
+
// — the repeated user-agent, accept and sec-ch-ua fields are most of a
|
|
58
|
+
// request and none of them are in the static table.
|
|
59
|
+
//
|
|
60
|
+
// It also sidesteps a second hazard for free. RFC 9204 §2.1.3 warns about
|
|
61
|
+
// flow-control deadlock: the encoder stream is flow controlled, so an
|
|
62
|
+
// insertion can stall, and a request that depends on it stalls with it.
|
|
63
|
+
// Here a stalled insertion is simply an unacknowledged one, and an
|
|
64
|
+
// unacknowledged entry is never referenced.
|
|
65
|
+
// ============================================================
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
// buildStaticLookup returns { full, name } — two separate maps, not one flat
|
|
69
|
+
// object. `full` is keyed by "name\0value" and `name` by the name alone, and
|
|
70
|
+
// treating the result as a single map makes every lookup return undefined:
|
|
71
|
+
// no crash, no error, just an encoder that silently never finds anything and
|
|
72
|
+
// emits literals for fields that are one byte in the static table.
|
|
73
|
+
var STATIC = buildStaticLookup(QPACK_STATIC, 0);
|
|
74
|
+
var STATIC_FULL = STATIC.full;
|
|
75
|
+
var STATIC_NAME = STATIC.name;
|
|
76
|
+
|
|
77
|
+
// entrySize and ENTRY_OVERHEAD come from hpack_primitives: RFC 7541 §4.1 and
|
|
78
|
+
// RFC 9204 §3.2.1 define them identically, and two copies of a constant both
|
|
79
|
+
// peers must agree on is exactly the kind of thing that drifts.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* createQpackTable(capacity)
|
|
84
|
+
*
|
|
85
|
+
* Entries are held newest-first, which matches how they are addressed: QPACK
|
|
86
|
+
* numbers entries by ABSOLUTE index — a counter of every insertion ever made
|
|
87
|
+
* on the connection — rather than by position. That indirection is what lets
|
|
88
|
+
* a header block name an entry without knowing how many have been evicted
|
|
89
|
+
* since, and it is why `insertCount` below is never reset.
|
|
90
|
+
*/
|
|
91
|
+
function createQpackTable(capacity) {
|
|
92
|
+
var entries = []; // newest first
|
|
93
|
+
var size = 0; // bytes, by the formula above
|
|
94
|
+
var insertCount = 0; // absolute, monotonic, never reset
|
|
95
|
+
|
|
96
|
+
function evictTo(limit) {
|
|
97
|
+
while (size > limit && entries.length > 0) {
|
|
98
|
+
var e = entries.pop();
|
|
99
|
+
size -= entrySize(e.name, e.value);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
/**
|
|
105
|
+
* Insert, returning the new absolute index, or null if it did not fit.
|
|
106
|
+
*
|
|
107
|
+
* An entry larger than the whole table is not an error: §3.2.2 says to
|
|
108
|
+
* empty the table and not insert it. Treating it as a failure would be
|
|
109
|
+
* wrong, and treating it as a success would leave both sides disagreeing
|
|
110
|
+
* about what the table holds.
|
|
111
|
+
*/
|
|
112
|
+
insert: function (name, value) {
|
|
113
|
+
var need = entrySize(name, value);
|
|
114
|
+
if (need > capacity) { evictTo(0); return null; }
|
|
115
|
+
evictTo(capacity - need);
|
|
116
|
+
entries.unshift({ name: name, value: value });
|
|
117
|
+
size += need;
|
|
118
|
+
return ++insertCount;
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
/** Absolute index -> entry, or null once it has been evicted. */
|
|
122
|
+
get: function (absoluteIndex) {
|
|
123
|
+
var pos = insertCount - absoluteIndex;
|
|
124
|
+
if (pos < 0 || pos >= entries.length) return null;
|
|
125
|
+
return entries[pos];
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Find a name/value pair, or just the name.
|
|
130
|
+
*
|
|
131
|
+
* Returns { index, full }, where `full` distinguishes a complete match
|
|
132
|
+
* from a name-only one — the difference between emitting one byte and
|
|
133
|
+
* emitting a name reference plus a literal value.
|
|
134
|
+
*/
|
|
135
|
+
find: function (name, value) {
|
|
136
|
+
var nameHit = null;
|
|
137
|
+
for (var i = 0; i < entries.length; i++) {
|
|
138
|
+
if (entries[i].name !== name) continue;
|
|
139
|
+
var absolute = insertCount - i;
|
|
140
|
+
if (entries[i].value === value) return { index: absolute, full: true };
|
|
141
|
+
if (nameHit === null) nameHit = { index: absolute, full: false };
|
|
142
|
+
}
|
|
143
|
+
return nameHit;
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Whether an entry fits WITHOUT evicting anything.
|
|
147
|
+
//
|
|
148
|
+
// The encoder only ever inserts when this holds. Evicting is where QPACK
|
|
149
|
+
// gets dangerous: RFC 9204 §2.1.1 forbids evicting an entry that a field
|
|
150
|
+
// section still in flight refers to, and knowing that means tracking every
|
|
151
|
+
// unacknowledged section. Never evicting sidesteps it entirely — once the
|
|
152
|
+
// table is full, new fields simply go out as literals. 4096 bytes holds
|
|
153
|
+
// every header that actually repeats.
|
|
154
|
+
fits: function (name, value) {
|
|
155
|
+
return size + entrySize(name, value) <= capacity;
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
setCapacity: function (n) { capacity = n; evictTo(capacity); },
|
|
159
|
+
|
|
160
|
+
get insertCount() { return insertCount; },
|
|
161
|
+
get size() { return size; },
|
|
162
|
+
get capacity() { return capacity; },
|
|
163
|
+
get length() { return entries.length; }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
// ============================================================
|
|
169
|
+
// Encoder insertion history
|
|
170
|
+
// ============================================================
|
|
171
|
+
//
|
|
172
|
+
// Which fields this encoder has sent recently, so that a field is inserted
|
|
173
|
+
// into the dynamic table on its SECOND sighting rather than its first. The
|
|
174
|
+
// table is never evicted (see fits above), so whatever enters it stays for
|
|
175
|
+
// the connection's life — and inserting on first sight filled it with values
|
|
176
|
+
// that never come back: on a connection of 300 API responses, 74 of the 79
|
|
177
|
+
// entries were one response's etag or x-request-id, the table was full after
|
|
178
|
+
// 37 responses, and from then on nothing new could be learned. A field seen
|
|
179
|
+
// twice is one that repeats.
|
|
180
|
+
//
|
|
181
|
+
// Bounded the way the table is, by bytes (entrySize, RFC 9204 §3.2.1), and
|
|
182
|
+
// forgets oldest first: what repeats does so from one response to the next,
|
|
183
|
+
// well within it.
|
|
184
|
+
function createQpackHistory(byteLimit) {
|
|
185
|
+
var seen = new Map(); // name\0value -> its size; insertion order is age
|
|
186
|
+
var size = 0;
|
|
187
|
+
return {
|
|
188
|
+
/** Whether the field was seen before; records it either way. */
|
|
189
|
+
seenBefore: function (name, value) {
|
|
190
|
+
var key = name + '\0' + value;
|
|
191
|
+
if (seen.has(key)) return true;
|
|
192
|
+
var need = entrySize(name, value);
|
|
193
|
+
if (need > byteLimit) return false; // could never be inserted anyway
|
|
194
|
+
seen.set(key, need);
|
|
195
|
+
size += need;
|
|
196
|
+
while (size > byteLimit) {
|
|
197
|
+
var oldest = seen.keys().next().value;
|
|
198
|
+
size -= seen.get(oldest);
|
|
199
|
+
seen.delete(oldest);
|
|
200
|
+
}
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
// ============================================================
|
|
208
|
+
// Encoder-stream instructions (RFC 9204 §4.3)
|
|
209
|
+
// ============================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* "Insert with Name Reference" (1Txxxxxx) or "Insert with Literal Name"
|
|
213
|
+
* (01Nxxxxx), whichever the static table allows.
|
|
214
|
+
*
|
|
215
|
+
* T=1 means the name index refers to the STATIC table. Only static references
|
|
216
|
+
* are used here: a dynamic name reference would create a dependency between
|
|
217
|
+
* two insertions, and while that is legal it earns very little and adds a
|
|
218
|
+
* second ordering constraint to reason about.
|
|
219
|
+
*/
|
|
220
|
+
function encodeInsert(name, value) {
|
|
221
|
+
var staticIndex = STATIC_NAME[name];
|
|
222
|
+
|
|
223
|
+
// encodePrefixedInt and encodeStringLiteral both return plain arrays, not
|
|
224
|
+
// Buffers — they are shared with HPACK, where the caller accumulates into an
|
|
225
|
+
// array and converts once at the end. Wrapping each one here rather than
|
|
226
|
+
// assuming.
|
|
227
|
+
if (staticIndex !== undefined) {
|
|
228
|
+
var prefix = encodePrefixedInt(staticIndex, 6);
|
|
229
|
+
prefix[0] |= 0xc0; // 1 1 xxxxxx : insert, static name
|
|
230
|
+
return Buffer.concat([Buffer.from(prefix),
|
|
231
|
+
Buffer.from(encodeStringLiteral(value, 7))]);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Insert with Literal Name: 01Nxxxxx, N=0 (this entry may be indexed).
|
|
235
|
+
// The name uses a 5-bit prefix here, not the 7-bit one used for values —
|
|
236
|
+
// the two are different fields and encoding them alike is a silent
|
|
237
|
+
// corruption that only shows on long names.
|
|
238
|
+
var nameBytes = encodeStringLiteral(name, 5);
|
|
239
|
+
nameBytes[0] |= 0x40;
|
|
240
|
+
return Buffer.concat([Buffer.from(nameBytes),
|
|
241
|
+
Buffer.from(encodeStringLiteral(value, 7))]);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** "Set Dynamic Table Capacity" (001xxxxx). */
|
|
245
|
+
function encodeSetCapacity(capacity) {
|
|
246
|
+
var b = encodePrefixedInt(capacity, 5);
|
|
247
|
+
b[0] |= 0x20;
|
|
248
|
+
return Buffer.from(b);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
// ============================================================
|
|
253
|
+
// Decoder-stream instructions (RFC 9204 §4.4)
|
|
254
|
+
// ============================================================
|
|
255
|
+
|
|
256
|
+
var DECODER_SECTION_ACK = 0x80; // 1xxxxxxx
|
|
257
|
+
var DECODER_STREAM_CANCEL = 0x40; // 01xxxxxx
|
|
258
|
+
var DECODER_INSERT_COUNT = 0x00; // 00xxxxxx
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Read the peer's decoder stream and report what it acknowledged.
|
|
262
|
+
*
|
|
263
|
+
* Only "Insert Count Increment" carries what this encoder needs: how many of
|
|
264
|
+
* our insertions the peer has processed. Section acknowledgements and stream
|
|
265
|
+
* cancellations matter for eviction accounting in a fuller implementation;
|
|
266
|
+
* here they are parsed and skipped, because entries are never evicted while
|
|
267
|
+
* referenced — this encoder only references acknowledged entries, and the
|
|
268
|
+
* table is small enough that eviction pressure comes from capacity alone.
|
|
269
|
+
*/
|
|
270
|
+
function parseDecoderInstructions(buf, offset) {
|
|
271
|
+
var increments = 0;
|
|
272
|
+
var i = offset || 0;
|
|
273
|
+
|
|
274
|
+
// readPrefixedInt(buf, prefixBits, pos) — prefix width SECOND, position
|
|
275
|
+
// THIRD, and it returns { value, next } rather than a byte length. Getting
|
|
276
|
+
// that order wrong is not a crash: it reads a plausible integer from the
|
|
277
|
+
// wrong bits and advances by the wrong amount, so the stream desynchronises
|
|
278
|
+
// and every instruction after the first is misread.
|
|
279
|
+
while (i < buf.length) {
|
|
280
|
+
var b = buf[i];
|
|
281
|
+
var r;
|
|
282
|
+
|
|
283
|
+
if (b & DECODER_SECTION_ACK) {
|
|
284
|
+
r = readPrefixedInt(buf, 7, i);
|
|
285
|
+
if (!r) break;
|
|
286
|
+
i = r.next;
|
|
287
|
+
} else if (b & DECODER_STREAM_CANCEL) {
|
|
288
|
+
r = readPrefixedInt(buf, 6, i);
|
|
289
|
+
if (!r) break;
|
|
290
|
+
i = r.next;
|
|
291
|
+
} else {
|
|
292
|
+
r = readPrefixedInt(buf, 6, i);
|
|
293
|
+
if (!r) break;
|
|
294
|
+
increments += r.value;
|
|
295
|
+
i = r.next;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { increments: increments, consumed: i - (offset || 0) };
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
// ============================================================
|
|
304
|
+
// Header block encoding (RFC 9204 §4.5)
|
|
305
|
+
// ============================================================
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Encode one header list.
|
|
309
|
+
*
|
|
310
|
+
* Returns { block, inserts } — the field section for the request stream, and
|
|
311
|
+
* any insertions that must go out on the encoder stream. The caller sends the
|
|
312
|
+
* insertions FIRST; they describe entries this block does not yet use, since
|
|
313
|
+
* nothing is referenced before it is acknowledged.
|
|
314
|
+
*
|
|
315
|
+
* `options.knownReceivedCount` is the acknowledgement watermark: entries at or
|
|
316
|
+
* below it are safe to reference, entries above it are not. It is the single
|
|
317
|
+
* value that makes this encoder non-blocking.
|
|
318
|
+
*/
|
|
319
|
+
function encodeHeaderBlock(headerList, table, options) {
|
|
320
|
+
options = options || {};
|
|
321
|
+
// Base: the acknowledged insert count. Everything referenced is at or below
|
|
322
|
+
// it, which is the whole non-blocking guarantee.
|
|
323
|
+
var known = options.knownReceivedCount || 0;
|
|
324
|
+
// MaxEntries for the Required Insert Count encoding comes from the DECODER's
|
|
325
|
+
// advertised SETTINGS_QPACK_MAX_TABLE_CAPACITY — the peer's — not from the
|
|
326
|
+
// capacity this encoder chose to use (RFC 9204 §4.5.1.1).
|
|
327
|
+
var maxEntries = Math.floor((options.peerMaxTableCapacity || 0) / ENTRY_OVERHEAD);
|
|
328
|
+
var neverIndex = options.neverIndex || {};
|
|
329
|
+
|
|
330
|
+
var out = [];
|
|
331
|
+
var inserts = [];
|
|
332
|
+
var queued = {}; // name\0value already queued in this block
|
|
333
|
+
var queuedSize = 0; // bytes those will take
|
|
334
|
+
var maxRef = 0; // highest absolute index referenced (1-based)
|
|
335
|
+
|
|
336
|
+
for (var i = 0; i < headerList.length; i++) {
|
|
337
|
+
var name = String(headerList[i][0]).toLowerCase();
|
|
338
|
+
var value = String(headerList[i][1]);
|
|
339
|
+
|
|
340
|
+
// ---- Static table, full match: one byte ----
|
|
341
|
+
var staticFull = STATIC_FULL[name + '\0' + value];
|
|
342
|
+
if (staticFull !== undefined) {
|
|
343
|
+
var idx = encodePrefixedInt(staticFull, 6);
|
|
344
|
+
idx[0] |= 0xc0; // 1 T=1 xxxxxx : indexed, static
|
|
345
|
+
push(out, idx);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// ---- Dynamic table, but only if acknowledged ----
|
|
350
|
+
//
|
|
351
|
+
// `hit.index <= known` and `rel >= 0` below are the same condition: rel
|
|
352
|
+
// is (known - index). Both are kept because they say different things —
|
|
353
|
+
// the first is the non-blocking policy, the second a bounds check on the
|
|
354
|
+
// wire encoding.
|
|
355
|
+
var neverIdx = neverIndex[name] === true || headerList[i][2] === true;
|
|
356
|
+
if (!neverIdx && table.capacity > 0 && maxEntries > 0) {
|
|
357
|
+
var hit = table.find(name, value);
|
|
358
|
+
if (hit && hit.full && hit.index <= known) {
|
|
359
|
+
// Indexed, dynamic: 10xxxxxx, relative to Base (§4.5.2). Absolute
|
|
360
|
+
// indices are zero-based in the RFC and one-based in this table, so
|
|
361
|
+
// relative = Base - 1 - (index - 1) = known - index.
|
|
362
|
+
var rel = known - hit.index;
|
|
363
|
+
if (rel >= 0) {
|
|
364
|
+
var dyn = encodePrefixedInt(rel, 6);
|
|
365
|
+
dyn[0] |= 0x80;
|
|
366
|
+
push(out, dyn);
|
|
367
|
+
if (hit.index > maxRef) maxRef = hit.index;
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
var staticName = STATIC_NAME[name];
|
|
374
|
+
|
|
375
|
+
if (neverIdx) {
|
|
376
|
+
// N=1: an intermediary must not add this to a table either. Compressed
|
|
377
|
+
// size is observable, so a secret in a table can be recovered one byte
|
|
378
|
+
// at a time — the CRIME attack, which is what the N bit exists for.
|
|
379
|
+
if (staticName !== undefined) {
|
|
380
|
+
var nl = encodePrefixedInt(staticName, 4);
|
|
381
|
+
nl[0] |= 0x70; // 01 N=1 T=1 xxxx : literal, static name
|
|
382
|
+
push(out, nl);
|
|
383
|
+
} else {
|
|
384
|
+
// 001 N=1 H xxx. The H bit (0x08) is set by encodeStringLiteral when
|
|
385
|
+
// it actually used Huffman; forcing it here — as 0x38 did — marked a
|
|
386
|
+
// plain name as Huffman-coded, and the peer decoded garbage.
|
|
387
|
+
var nn = encodeStringLiteral(name, 3);
|
|
388
|
+
nn[0] |= 0x30;
|
|
389
|
+
push(out, nn);
|
|
390
|
+
}
|
|
391
|
+
push(out, encodeStringLiteral(value, 7));
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (staticName !== undefined) {
|
|
396
|
+
var ln = encodePrefixedInt(staticName, 4);
|
|
397
|
+
ln[0] |= 0x50; // 01 N=0 T=1 xxxx : literal, static name
|
|
398
|
+
push(out, ln);
|
|
399
|
+
push(out, encodeStringLiteral(value, 7));
|
|
400
|
+
} else {
|
|
401
|
+
var lit = encodeStringLiteral(name, 3);
|
|
402
|
+
lit[0] |= 0x20; // 001 N=0 H xxx : literal, literal name
|
|
403
|
+
push(out, lit);
|
|
404
|
+
push(out, encodeStringLiteral(value, 7));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Queue it so a LATER request can reference it — only if it is not in the
|
|
408
|
+
// table already, not queued twice in one block, fits without evicting,
|
|
409
|
+
// and (given a history) has been sent before: see createQpackHistory.
|
|
410
|
+
var key = name + '\0' + value;
|
|
411
|
+
if (table.capacity > 0 && maxEntries > 0 && !queued[key] &&
|
|
412
|
+
!(table.find(name, value) || {}).full &&
|
|
413
|
+
(!options.history || options.history.seenBefore(name, value))) {
|
|
414
|
+
var need = entrySize(name, value);
|
|
415
|
+
if (table.size + queuedSize + need <= table.capacity) {
|
|
416
|
+
queued[key] = true;
|
|
417
|
+
queuedSize += need;
|
|
418
|
+
inserts.push({ name: name, value: value });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Field Section Prefix (§4.5.1).
|
|
424
|
+
//
|
|
425
|
+
// Required Insert Count is the highest entry THIS section references, plus
|
|
426
|
+
// one — not everything acknowledged so far. A section that references
|
|
427
|
+
// nothing declares 0, and depends on nothing.
|
|
428
|
+
//
|
|
429
|
+
// Base stays at the acknowledged count, which is at or above it, so the
|
|
430
|
+
// Delta Base is (Base - RIC) with the sign bit clear.
|
|
431
|
+
var prefix = [];
|
|
432
|
+
if (maxRef === 0) {
|
|
433
|
+
push(prefix, encodePrefixedInt(0, 8));
|
|
434
|
+
push(prefix, encodePrefixedInt(0, 7));
|
|
435
|
+
} else {
|
|
436
|
+
push(prefix, encodeRequiredInsertCount(maxRef, maxEntries));
|
|
437
|
+
push(prefix, encodePrefixedInt(known - maxRef, 7)); // S=0
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return {
|
|
441
|
+
block: Buffer.from(prefix.concat(out)),
|
|
442
|
+
inserts: inserts
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Required Insert Count, encoded modulo 2 * MaxEntries (§4.5.1.1).
|
|
448
|
+
*
|
|
449
|
+
* The wrapping exists so the field stays small on long-lived connections
|
|
450
|
+
* where the absolute count grows without bound. With capacity 0 there are no
|
|
451
|
+
* dynamic references at all and the value is 0.
|
|
452
|
+
*/
|
|
453
|
+
function encodeRequiredInsertCount(ric, maxEntries) {
|
|
454
|
+
// RFC 9204 §4.5.1.1: 0 stays 0; otherwise (RIC mod 2*MaxEntries) + 1.
|
|
455
|
+
//
|
|
456
|
+
// This sent the count raw for a while, to agree with quico's own decoder,
|
|
457
|
+
// which read it raw. The two agreed with each other and with nothing else:
|
|
458
|
+
// Chrome and ls-qpack decode the wrapped form, as the RFC requires.
|
|
459
|
+
if (ric === 0 || maxEntries === 0) return encodePrefixedInt(0, 8);
|
|
460
|
+
return encodePrefixedInt((ric % (2 * maxEntries)) + 1, 8);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* The inverse, for the decoder: the Required Insert Count a field section
|
|
465
|
+
* actually declares (RFC 9204 §4.5.1.1).
|
|
466
|
+
*
|
|
467
|
+
* `totalInserts` is how many entries the decoder has received so far and
|
|
468
|
+
* `maxEntries` is floor(the capacity WE advertised / 32). Throws on an
|
|
469
|
+
* encoding no conforming encoder could have produced.
|
|
470
|
+
*/
|
|
471
|
+
function decodeRequiredInsertCount(encoded, maxEntries, totalInserts) {
|
|
472
|
+
if (encoded === 0) return 0;
|
|
473
|
+
var fullRange = 2 * maxEntries;
|
|
474
|
+
if (maxEntries === 0 || encoded > fullRange) {
|
|
475
|
+
throw new Error('QPACK: invalid Required Insert Count encoding ' + encoded);
|
|
476
|
+
}
|
|
477
|
+
var maxValue = totalInserts + maxEntries;
|
|
478
|
+
var maxWrapped = Math.floor(maxValue / fullRange) * fullRange;
|
|
479
|
+
var ric = maxWrapped + encoded - 1;
|
|
480
|
+
if (ric > maxValue) {
|
|
481
|
+
if (ric <= fullRange) throw new Error('QPACK: invalid Required Insert Count ' + ric);
|
|
482
|
+
ric -= fullRange;
|
|
483
|
+
}
|
|
484
|
+
if (ric === 0) throw new Error('QPACK: Required Insert Count decoded to 0');
|
|
485
|
+
return ric;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/** "Section Acknowledgment" (1xxxxxxx) for the decoder stream (§4.4.1). */
|
|
489
|
+
function encodeSectionAck(streamId) {
|
|
490
|
+
var b = encodePrefixedInt(streamId, 7);
|
|
491
|
+
b[0] |= 0x80;
|
|
492
|
+
return Buffer.from(b);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* The Base this encoder uses, and what the decoder must agree it is.
|
|
498
|
+
*
|
|
499
|
+
* Kept equal to the acknowledged insert count, which is also the Required
|
|
500
|
+
* Insert Count above. Everything referenced is at or below it by construction,
|
|
501
|
+
* so no post-base indices are ever emitted and the Delta Base is always zero.
|
|
502
|
+
*/
|
|
503
|
+
|
|
504
|
+
/** Delta Base, sign bit clear: Base equals Required Insert Count. */
|
|
505
|
+
function encodeBase(delta) {
|
|
506
|
+
var b = encodePrefixedInt(delta, 7);
|
|
507
|
+
return b; // S=0
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function push(arr, bytes) {
|
|
511
|
+
for (var i = 0; i < bytes.length; i++) arr.push(bytes[i]);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* "Insert Count Increment" (00xxxxxx) for the decoder stream.
|
|
517
|
+
*
|
|
518
|
+
* Sent after processing insertions from the peer's encoder stream. Without
|
|
519
|
+
* it a conservative peer — one that only references acknowledged entries,
|
|
520
|
+
* like this implementation — never advances past literals, and the dynamic
|
|
521
|
+
* table on both sides fills up while compressing nothing. The two halves are
|
|
522
|
+
* useless apart.
|
|
523
|
+
*/
|
|
524
|
+
/**
|
|
525
|
+
* "Stream Cancellation" (01xxxxxx) for the decoder stream (RFC 9204 §4.4.2):
|
|
526
|
+
* reading of a stream was abandoned, and any section on it will never be
|
|
527
|
+
* acknowledged.
|
|
528
|
+
*/
|
|
529
|
+
function encodeStreamCancellation(streamId) {
|
|
530
|
+
var b = encodePrefixedInt(streamId, 6);
|
|
531
|
+
b[0] |= 0x40;
|
|
532
|
+
return Buffer.from(b);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
function encodeInsertCountIncrement(count) {
|
|
536
|
+
var b = encodePrefixedInt(count, 6);
|
|
537
|
+
b[0] &= 0x3f; // 00xxxxxx
|
|
538
|
+
return Buffer.from(b);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
export {
|
|
543
|
+
createQpackHistory,
|
|
544
|
+
encodeStreamCancellation,
|
|
545
|
+
encodeInsertCountIncrement,
|
|
546
|
+
decodeRequiredInsertCount,
|
|
547
|
+
encodeSectionAck,
|
|
548
|
+
createQpackTable,
|
|
549
|
+
encodeHeaderBlock,
|
|
550
|
+
encodeInsert,
|
|
551
|
+
encodeSetCapacity,
|
|
552
|
+
parseDecoderInstructions,
|
|
553
|
+
entrySize,
|
|
554
|
+
ENTRY_OVERHEAD
|
|
555
|
+
};
|