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
|
@@ -0,0 +1,520 @@
|
|
|
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
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// Field-compression primitives shared by HPACK and QPACK.
|
|
18
|
+
//
|
|
19
|
+
// QPACK (RFC 9204, HTTP/3) is a reworking of HPACK (RFC 7541, HTTP/2) for
|
|
20
|
+
// out-of-order delivery. The two differ in how the dynamic table is indexed
|
|
21
|
+
// and updated — but the *wire primitives* underneath are the same document:
|
|
22
|
+
//
|
|
23
|
+
// - Huffman code RFC 7541 Appendix B (QPACK cites it verbatim)
|
|
24
|
+
// - Prefixed integers RFC 7541 §5.1 (QPACK §4.1.1: identical)
|
|
25
|
+
// - String literals RFC 7541 §5.2 (QPACK §4.1.2: identical)
|
|
26
|
+
//
|
|
27
|
+
// Only the static tables differ (RFC 7541 Appendix A vs RFC 9204 Appendix A),
|
|
28
|
+
// and even those are built and searched the same way — hence the shared
|
|
29
|
+
// buildStaticLookup() below.
|
|
30
|
+
//
|
|
31
|
+
// The file is named for HPACK because HPACK is where these primitives are
|
|
32
|
+
// *defined*; QPACK imports them by reference. Nothing here is HTTP/2-specific.
|
|
33
|
+
// ============================================================
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
// ============================================================
|
|
37
|
+
// Huffman (RFC 7541 Appendix B)
|
|
38
|
+
//
|
|
39
|
+
// 257 entries: symbols 0..255 plus EOS at 256. EOS never appears in a
|
|
40
|
+
// well-formed encoding — it exists only as the source of the padding bit
|
|
41
|
+
// pattern (its code is all-ones), so decoding it is a protocol error.
|
|
42
|
+
// ============================================================
|
|
43
|
+
|
|
44
|
+
var HUFF_CODES = new Uint32Array([
|
|
45
|
+
0x1ff8,0x7fffd8,0xfffffe2,0xfffffe3,0xfffffe4,0xfffffe5,0xfffffe6,0xfffffe7,
|
|
46
|
+
0xfffffe8,0xffffea,0x3ffffffc,0xfffffe9,0xfffffea,0x3ffffffd,0xfffffeb,0xfffffec,
|
|
47
|
+
0xfffffed,0xfffffee,0xfffffef,0xffffff0,0xffffff1,0xffffff2,0x3ffffffe,0xffffff3,
|
|
48
|
+
0xffffff4,0xffffff5,0xffffff6,0xffffff7,0xffffff8,0xffffff9,0xffffffa,0xffffffb,
|
|
49
|
+
0x14,0x3f8,0x3f9,0xffa,0x1ff9,0x15,0xf8,0x7fa,0x3fa,0x3fb,0xf9,0x7fb,0xfa,
|
|
50
|
+
0x16,0x17,0x18,0x0,0x1,0x2,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f,0x5c,0xfb,
|
|
51
|
+
0x7ffc,0x20,0xffb,0x3fc,0x1ffa,0x21,0x5d,0x5e,0x5f,0x60,0x61,0x62,0x63,0x64,
|
|
52
|
+
0x65,0x66,0x67,0x68,0x69,0x6a,0x6b,0x6c,0x6d,0x6e,0x6f,0x70,0x71,0x72,0xfc,
|
|
53
|
+
0x73,0xfd,0x1ffb,0x7fff0,0x1ffc,0x3ffc,0x22,0x7ffd,0x3,0x23,0x4,0x24,0x5,0x25,
|
|
54
|
+
0x26,0x27,0x6,0x74,0x75,0x28,0x29,0x2a,0x7,0x2b,0x76,0x2c,0x8,0x9,0x2d,0x77,
|
|
55
|
+
0x78,0x79,0x7a,0x7b,0x7ffe,0x7fc,0x3ffd,0x1ffd,0xffffffc,0xfffe6,0x3fffd2,
|
|
56
|
+
0xfffe7,0xfffe8,0x3fffd3,0x3fffd4,0x3fffd5,0x7fffd9,0x3fffd6,0x7fffda,0x7fffdb,
|
|
57
|
+
0x7fffdc,0x7fffdd,0x7fffde,0xffffeb,0x7fffdf,0xffffec,0xffffed,0x3fffd7,0x7fffe0,
|
|
58
|
+
0xffffee,0x7fffe1,0x7fffe2,0x7fffe3,0x7fffe4,0x1fffdc,0x3fffd8,0x7fffe5,0x3fffd9,
|
|
59
|
+
0x7fffe6,0x7fffe7,0xffffef,0x3fffda,0x1fffdd,0xfffe9,0x3fffdb,0x3fffdc,0x7fffe8,
|
|
60
|
+
0x7fffe9,0x1fffde,0x7fffea,0x3fffdd,0x3fffde,0xfffff0,0x1fffdf,0x3fffdf,0x7fffeb,
|
|
61
|
+
0x7fffec,0x1fffe0,0x1fffe1,0x3fffe0,0x1fffe2,0x7fffed,0x3fffe1,0x7fffee,0x7fffef,
|
|
62
|
+
0xfffea,0x3fffe2,0x3fffe3,0x3fffe4,0x7ffff0,0x3fffe5,0x3fffe6,0x7ffff1,0x3ffffe0,
|
|
63
|
+
0x3ffffe1,0xfffeb,0x7fff1,0x3fffe7,0x7ffff2,0x3fffe8,0x1ffffec,0x3ffffe2,0x3ffffe3,
|
|
64
|
+
0x3ffffe4,0x7ffffde,0x7ffffdf,0x3ffffe5,0xfffff1,0x1ffffed,0x7fff2,0x1fffe3,
|
|
65
|
+
0x3ffffe6,0x7ffffe0,0x7ffffe1,0x3ffffe7,0x7ffffe2,0xfffff2,0x1fffe4,0x1fffe5,
|
|
66
|
+
0x3ffffe8,0x3ffffe9,0xffffffd,0x7ffffe3,0x7ffffe4,0x7ffffe5,0xfffec,0xfffff3,
|
|
67
|
+
0xfffed,0x1fffe6,0x3fffe9,0x1fffe7,0x1fffe8,0x7ffff3,0x3fffea,0x3fffeb,0x1ffffee,
|
|
68
|
+
0x1ffffef,0xfffff4,0xfffff5,0x3ffffea,0x7ffff4,0x3ffffeb,0x7ffffe6,0x3ffffec,
|
|
69
|
+
0x3ffffed,0x7ffffe7,0x7ffffe8,0x7ffffe9,0x7ffffea,0x7ffffeb,0xffffffe,0x7ffffec,
|
|
70
|
+
0x7ffffed,0x7ffffee,0x7ffffef,0x7fffff0,0x3ffffee,0x3fffffff
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
var HUFF_BITS = new Uint8Array([
|
|
74
|
+
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,
|
|
75
|
+
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,
|
|
76
|
+
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,
|
|
77
|
+
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,
|
|
78
|
+
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,
|
|
79
|
+
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,
|
|
80
|
+
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,
|
|
81
|
+
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,
|
|
82
|
+
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,
|
|
83
|
+
28,27,27,27,27,27,26,30
|
|
84
|
+
]);
|
|
85
|
+
|
|
86
|
+
var HUFF_EOS = 256; // index of the EOS symbol; decoding it is an error
|
|
87
|
+
|
|
88
|
+
var _huffRoot = (function () {
|
|
89
|
+
var root = {};
|
|
90
|
+
for (var i = 0; i < HUFF_CODES.length; i++) {
|
|
91
|
+
var code = HUFF_CODES[i], bits = HUFF_BITS[i], node = root;
|
|
92
|
+
for (var j = bits - 1; j >= 0; j--) {
|
|
93
|
+
var bit = (code >> j) & 1;
|
|
94
|
+
if (!node[bit]) node[bit] = {};
|
|
95
|
+
node = node[bit];
|
|
96
|
+
}
|
|
97
|
+
node.sym = i;
|
|
98
|
+
}
|
|
99
|
+
return root;
|
|
100
|
+
})();
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Decode a Huffman-coded byte string (RFC 7541 §5.2). Returns the raw
|
|
105
|
+
* Uint8Array, NOT a string.
|
|
106
|
+
*
|
|
107
|
+
* Field names and values are opaque octets on the wire, and the two callers
|
|
108
|
+
* disagree about how to turn them into text — so that decision stays with
|
|
109
|
+
* them. (QPACK in this codebase has always used TextDecoder/UTF-8; a stricter
|
|
110
|
+
* HPACK may prefer a lossless latin-1 mapping. Baking either one in here would
|
|
111
|
+
* silently change the other.)
|
|
112
|
+
*
|
|
113
|
+
* `strict` turns on the three padding checks the RFC mandates (§5.2): padding
|
|
114
|
+
* longer than 7 bits, padding that is not the EOS prefix (all ones), and an
|
|
115
|
+
* actual EOS symbol in the stream must all be treated as errors. They are off
|
|
116
|
+
* by default so that existing callers keep their current lenient behaviour;
|
|
117
|
+
* decoders that must answer a malformed peer with COMPRESSION_ERROR pass true.
|
|
118
|
+
*/
|
|
119
|
+
function huffDecode(buf, strict) {
|
|
120
|
+
var out = [];
|
|
121
|
+
var node = _huffRoot;
|
|
122
|
+
var nbits = 0, current = 0;
|
|
123
|
+
var sincePadStart = 0; // bits consumed since the last complete symbol
|
|
124
|
+
var padIsOnes = true; // were all of those bits 1?
|
|
125
|
+
|
|
126
|
+
for (var i = 0; i < buf.length; i++) {
|
|
127
|
+
current = (current << 8) | buf[i];
|
|
128
|
+
nbits += 8;
|
|
129
|
+
while (nbits > 0) {
|
|
130
|
+
var bit = (current >> (nbits - 1)) & 1;
|
|
131
|
+
node = node[bit];
|
|
132
|
+
if (!node) throw new Error('Invalid Huffman code');
|
|
133
|
+
nbits--;
|
|
134
|
+
sincePadStart++;
|
|
135
|
+
if (bit === 0) padIsOnes = false;
|
|
136
|
+
if (node.sym !== undefined) {
|
|
137
|
+
if (strict && node.sym === HUFF_EOS) {
|
|
138
|
+
throw new Error('Huffman: EOS symbol in input');
|
|
139
|
+
}
|
|
140
|
+
out.push(node.sym);
|
|
141
|
+
node = _huffRoot;
|
|
142
|
+
sincePadStart = 0;
|
|
143
|
+
padIsOnes = true;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (strict && node !== _huffRoot) {
|
|
149
|
+
// Trailing bits that did not complete a symbol are padding. They must be
|
|
150
|
+
// at most 7 bits and must be the most-significant bits of the EOS code.
|
|
151
|
+
if (sincePadStart > 7) throw new Error('Huffman: padding longer than 7 bits');
|
|
152
|
+
if (!padIsOnes) throw new Error('Huffman: padding is not the EOS prefix');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return Uint8Array.from(out);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
/** Byte length huffEncode() would produce, without producing it. */
|
|
160
|
+
function huffEncodedLength(bytes) {
|
|
161
|
+
var bits = 0;
|
|
162
|
+
for (var i = 0; i < bytes.length; i++) bits += HUFF_BITS[bytes[i]];
|
|
163
|
+
return (bits + 7) >> 3;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
// Powers of two as floats, used wherever a shift would exceed 32 bits.
|
|
168
|
+
// huffEncode accumulates up to 7 leftover bits plus one 30-bit code = 37 bits;
|
|
169
|
+
// readPrefixedInt walks a continuation chain whose shift reaches 7 bits per
|
|
170
|
+
// byte, so the table has to cover the full continuation limit below (an
|
|
171
|
+
// undersized table silently yields undefined → NaN, which readPrefixedInt then
|
|
172
|
+
// reports as "too large").
|
|
173
|
+
var _POW2 = (function () {
|
|
174
|
+
var p = new Float64Array(80);
|
|
175
|
+
for (var i = 0; i < 80; i++) p[i] = Math.pow(2, i);
|
|
176
|
+
return p;
|
|
177
|
+
})();
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Huffman-encode a byte array (RFC 7541 §5.2). Pads the final byte with the
|
|
182
|
+
* most-significant bits of the EOS code, i.e. ones.
|
|
183
|
+
*/
|
|
184
|
+
function huffEncode(bytes) {
|
|
185
|
+
var out = new Uint8Array(huffEncodedLength(bytes));
|
|
186
|
+
var pos = 0;
|
|
187
|
+
var cur = 0, nbits = 0;
|
|
188
|
+
|
|
189
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
190
|
+
var sym = bytes[i];
|
|
191
|
+
cur = cur * _POW2[HUFF_BITS[sym]] + HUFF_CODES[sym];
|
|
192
|
+
nbits += HUFF_BITS[sym];
|
|
193
|
+
while (nbits >= 8) {
|
|
194
|
+
var shift = _POW2[nbits - 8];
|
|
195
|
+
out[pos++] = Math.floor(cur / shift) & 0xff;
|
|
196
|
+
cur = cur % shift;
|
|
197
|
+
nbits -= 8;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (nbits > 0) {
|
|
202
|
+
var pad = 8 - nbits;
|
|
203
|
+
out[pos++] = ((cur * _POW2[pad]) + (_POW2[pad] - 1)) & 0xff;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
// ============================================================
|
|
211
|
+
// Prefixed integers (RFC 7541 §5.1, QPACK §4.1.1)
|
|
212
|
+
// ============================================================
|
|
213
|
+
|
|
214
|
+
// A malicious peer can send an unbounded run of continuation bytes to make the
|
|
215
|
+
// decoder spin. RFC 7541 §5.1 notes that implementations must limit the value
|
|
216
|
+
// they will decode; ten continuation bytes already exceed 2^53, so anything
|
|
217
|
+
// longer is either an attack or a value we could not represent anyway.
|
|
218
|
+
var _MAX_CONTINUATION_BYTES = 10;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Read a prefixed integer at buf[pos].
|
|
222
|
+
* Returns { value, next } or null if the buffer ends mid-integer (truncated).
|
|
223
|
+
*
|
|
224
|
+
* Used by the QPACK header-block parser, the QPACK encoder-instruction parser,
|
|
225
|
+
* and the HPACK header-block parser — the encoding is byte-identical in all
|
|
226
|
+
* three.
|
|
227
|
+
*/
|
|
228
|
+
function readPrefixedInt(buf, prefixBits, pos) {
|
|
229
|
+
if (pos >= buf.length) return null;
|
|
230
|
+
var max = (1 << prefixBits) - 1;
|
|
231
|
+
var value = buf[pos] & max;
|
|
232
|
+
pos++;
|
|
233
|
+
if (value < max) return { value: value, next: pos };
|
|
234
|
+
|
|
235
|
+
var m = 0;
|
|
236
|
+
var seen = 0;
|
|
237
|
+
while (pos < buf.length) {
|
|
238
|
+
var byte = buf[pos++];
|
|
239
|
+
// Multiplication rather than `<< m`: m reaches 28+ on large values, where
|
|
240
|
+
// the 32-bit shift wraps and silently produces a wrong (often negative)
|
|
241
|
+
// integer.
|
|
242
|
+
value += (byte & 0x7f) * _POW2[m];
|
|
243
|
+
if ((byte & 0x80) === 0) {
|
|
244
|
+
if (!isFinite(value) || value > Number.MAX_SAFE_INTEGER) {
|
|
245
|
+
throw new Error('Prefixed integer too large');
|
|
246
|
+
}
|
|
247
|
+
return { value: value, next: pos };
|
|
248
|
+
}
|
|
249
|
+
m += 7;
|
|
250
|
+
if (++seen > _MAX_CONTINUATION_BYTES) {
|
|
251
|
+
throw new Error('Prefixed integer too long');
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return null; // truncated mid-integer
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Encode a prefixed integer (RFC 7541 §5.1). Returns a plain Array of bytes;
|
|
260
|
+
* the caller ORs its opcode bits into byte 0, which is why the high bits above
|
|
261
|
+
* the prefix are left clear here.
|
|
262
|
+
*/
|
|
263
|
+
function encodePrefixedInt(value, prefixBits) {
|
|
264
|
+
var max = (1 << prefixBits) - 1;
|
|
265
|
+
if (value < max) return [value];
|
|
266
|
+
var bytes = [max];
|
|
267
|
+
value -= max;
|
|
268
|
+
while (value >= 128) {
|
|
269
|
+
bytes.push((value % 128) | 0x80);
|
|
270
|
+
value = Math.floor(value / 128);
|
|
271
|
+
}
|
|
272
|
+
bytes.push(value);
|
|
273
|
+
return bytes;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
// ============================================================
|
|
278
|
+
// String literals (RFC 7541 §5.2, QPACK §4.1.2)
|
|
279
|
+
//
|
|
280
|
+
// Layout: one bit H (Huffman?), then a (prefixBits)-wide prefixed integer
|
|
281
|
+
// holding the byte length, then that many bytes. H sits immediately above the
|
|
282
|
+
// length prefix, so its mask is 1 << prefixBits — 0x80 for the usual 7-bit
|
|
283
|
+
// prefix, 0x08 for the 3-bit prefix QPACK uses for literal names.
|
|
284
|
+
// ============================================================
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Decode a string literal at buf[pos].
|
|
288
|
+
* Returns { bytes, huffman, next }, or null if the literal is truncated.
|
|
289
|
+
*
|
|
290
|
+
* `bytes` is a Uint8Array for the same reason huffDecode returns one: the text
|
|
291
|
+
* codec is the caller's decision.
|
|
292
|
+
*/
|
|
293
|
+
function decodeStringLiteral(buf, prefixBits, pos, strict) {
|
|
294
|
+
if (pos >= buf.length) return null;
|
|
295
|
+
var huffman = (buf[pos] & (1 << prefixBits)) !== 0;
|
|
296
|
+
var len = readPrefixedInt(buf, prefixBits, pos);
|
|
297
|
+
if (!len) return null;
|
|
298
|
+
pos = len.next;
|
|
299
|
+
if (pos + len.value > buf.length) return null; // truncated payload
|
|
300
|
+
var raw = buf.subarray(pos, pos + len.value);
|
|
301
|
+
pos += len.value;
|
|
302
|
+
return {
|
|
303
|
+
bytes: huffman ? huffDecode(raw, strict) : raw,
|
|
304
|
+
huffman: huffman,
|
|
305
|
+
next: pos
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Encode a string literal. Returns a plain Array of bytes whose byte 0 already
|
|
312
|
+
* carries the H bit; the caller ORs in the opcode bits above it.
|
|
313
|
+
*
|
|
314
|
+
* `input` may be a Uint8Array (used as-is) or a string (UTF-8 encoded).
|
|
315
|
+
*
|
|
316
|
+
* useHuffman:
|
|
317
|
+
* undefined — Huffman only when it actually shrinks the string. This is what
|
|
318
|
+
* browsers and nghttp2 do: Huffman can *grow* a run of unusual
|
|
319
|
+
* bytes, and an encoder is free to send either form.
|
|
320
|
+
* true/false — force. `false` is what QPACK here has always done; leaving it
|
|
321
|
+
* that way keeps the bytes on the wire unchanged.
|
|
322
|
+
*/
|
|
323
|
+
function encodeStringLiteral(input, prefixBits, useHuffman) {
|
|
324
|
+
var raw = (typeof input === 'string') ? _utf8(input) : input;
|
|
325
|
+
var body = raw;
|
|
326
|
+
var huffman = false;
|
|
327
|
+
|
|
328
|
+
if (useHuffman !== false) {
|
|
329
|
+
var encoded = huffEncode(raw);
|
|
330
|
+
if (useHuffman === true || encoded.length < raw.length) {
|
|
331
|
+
body = encoded;
|
|
332
|
+
huffman = true;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
var out = encodePrefixedInt(body.length, prefixBits);
|
|
337
|
+
if (huffman) out[0] |= (1 << prefixBits);
|
|
338
|
+
for (var i = 0; i < body.length; i++) out.push(body[i]);
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
// ============================================================
|
|
344
|
+
// Static tables
|
|
345
|
+
// ============================================================
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* HPACK static table — RFC 7541 Appendix A. 61 entries, indexed from 1.
|
|
349
|
+
* Stored 0-based here; buildStaticLookup(HPACK_STATIC, 1) yields wire indices.
|
|
350
|
+
*/
|
|
351
|
+
var HPACK_STATIC = [
|
|
352
|
+
[':authority',''], [':method','GET'], [':method','POST'], [':path','/'],
|
|
353
|
+
[':path','/index.html'], [':scheme','http'], [':scheme','https'],
|
|
354
|
+
[':status','200'], [':status','204'], [':status','206'], [':status','304'],
|
|
355
|
+
[':status','400'], [':status','404'], [':status','500'],
|
|
356
|
+
['accept-charset',''], ['accept-encoding','gzip, deflate'],
|
|
357
|
+
['accept-language',''], ['accept-ranges',''], ['accept',''],
|
|
358
|
+
['access-control-allow-origin',''], ['age',''], ['allow',''],
|
|
359
|
+
['authorization',''], ['cache-control',''], ['content-disposition',''],
|
|
360
|
+
['content-encoding',''], ['content-language',''], ['content-length',''],
|
|
361
|
+
['content-location',''], ['content-range',''], ['content-type',''],
|
|
362
|
+
['cookie',''], ['date',''], ['etag',''], ['expect',''], ['expires',''],
|
|
363
|
+
['from',''], ['host',''], ['if-match',''], ['if-modified-since',''],
|
|
364
|
+
['if-none-match',''], ['if-range',''], ['if-unmodified-since',''],
|
|
365
|
+
['last-modified',''], ['link',''], ['location',''], ['max-forwards',''],
|
|
366
|
+
['proxy-authenticate',''], ['proxy-authorization',''], ['range',''],
|
|
367
|
+
['referer',''], ['refresh',''], ['retry-after',''], ['server',''],
|
|
368
|
+
['set-cookie',''], ['strict-transport-security',''],
|
|
369
|
+
['transfer-encoding',''], ['user-agent',''], ['vary',''], ['via',''],
|
|
370
|
+
['www-authenticate','']
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* QPACK static table — RFC 9204 Appendix A. 99 entries, indexed from 0.
|
|
375
|
+
* Unrelated to the HPACK table above: different membership, different order.
|
|
376
|
+
*/
|
|
377
|
+
var QPACK_STATIC = [
|
|
378
|
+
[':authority',''], [':path','/'], ['age','0'], ['content-disposition',''],
|
|
379
|
+
['content-length','0'], ['cookie',''], ['date',''], ['etag',''],
|
|
380
|
+
['if-modified-since',''], ['if-none-match',''], ['last-modified',''],
|
|
381
|
+
['link',''], ['location',''], ['referer',''], ['set-cookie',''],
|
|
382
|
+
[':method','CONNECT'], [':method','DELETE'], [':method','GET'],
|
|
383
|
+
[':method','HEAD'], [':method','OPTIONS'], [':method','POST'],
|
|
384
|
+
[':method','PUT'], [':scheme','http'], [':scheme','https'],
|
|
385
|
+
[':status','103'], [':status','200'], [':status','304'],
|
|
386
|
+
[':status','404'], [':status','503'], ['accept','*/*'],
|
|
387
|
+
['accept','application/dns-message'], ['accept-encoding','gzip, deflate, br'],
|
|
388
|
+
['accept-ranges','bytes'], ['access-control-allow-headers','cache-control'],
|
|
389
|
+
['access-control-allow-headers','content-type'],
|
|
390
|
+
['access-control-allow-origin','*'], ['cache-control','max-age=0'],
|
|
391
|
+
['cache-control','max-age=2592000'], ['cache-control','max-age=604800'],
|
|
392
|
+
['cache-control','no-cache'], ['cache-control','no-store'],
|
|
393
|
+
['cache-control','public, max-age=31536000'], ['content-encoding','br'],
|
|
394
|
+
['content-encoding','gzip'], ['content-type','application/dns-message'],
|
|
395
|
+
['content-type','application/javascript'], ['content-type','application/json'],
|
|
396
|
+
['content-type','application/x-www-form-urlencoded'],
|
|
397
|
+
['content-type','image/gif'], ['content-type','image/jpeg'],
|
|
398
|
+
['content-type','image/png'], ['content-type','text/css'],
|
|
399
|
+
['content-type','text/html; charset=utf-8'], ['content-type','text/plain'],
|
|
400
|
+
['content-type','text/plain;charset=utf-8'], ['range','bytes=0-'],
|
|
401
|
+
['strict-transport-security','max-age=31536000'],
|
|
402
|
+
['strict-transport-security','max-age=31536000; includesubdomains'],
|
|
403
|
+
['strict-transport-security','max-age=31536000; includesubdomains; preload'],
|
|
404
|
+
['vary','accept-encoding'], ['vary','origin'],
|
|
405
|
+
['x-content-type-options','nosniff'], ['x-xss-protection','1; mode=block'],
|
|
406
|
+
[':status','100'], [':status','204'], [':status','206'], [':status','302'],
|
|
407
|
+
[':status','400'], [':status','403'], [':status','421'], [':status','425'],
|
|
408
|
+
[':status','500'], ['accept-language',''],
|
|
409
|
+
['access-control-allow-credentials','FALSE'],
|
|
410
|
+
['access-control-allow-credentials','TRUE'],
|
|
411
|
+
['access-control-allow-headers','*'],
|
|
412
|
+
['access-control-allow-methods','get'],
|
|
413
|
+
['access-control-allow-methods','get, post, options'],
|
|
414
|
+
['access-control-allow-methods','options'],
|
|
415
|
+
['access-control-expose-headers','content-length'],
|
|
416
|
+
['access-control-request-headers','content-type'],
|
|
417
|
+
['access-control-request-method','get'],
|
|
418
|
+
['access-control-request-method','post'], ['alt-svc','clear'],
|
|
419
|
+
['authorization',''],
|
|
420
|
+
['content-security-policy',"script-src 'none'; object-src 'none'; base-uri 'none'"],
|
|
421
|
+
['early-data','1'], ['expect-ct',''], ['forwarded',''], ['if-range',''],
|
|
422
|
+
['origin',''], ['purpose','prefetch'], ['server',''],
|
|
423
|
+
['timing-allow-origin','*'], ['upgrade-insecure-requests','1'],
|
|
424
|
+
['user-agent',''], ['x-forwarded-for',''],
|
|
425
|
+
['x-frame-options','deny'], ['x-frame-options','sameorigin']
|
|
426
|
+
];
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Build the two lookup maps an encoder needs for a static table:
|
|
431
|
+
* full: "name\0value" → index (exact entry → single indexed field)
|
|
432
|
+
* name: "name" → index (name only → literal with name reference)
|
|
433
|
+
*
|
|
434
|
+
* `offset` is added to every index: 0 for QPACK (indexed from 0), 1 for HPACK
|
|
435
|
+
* (indexed from 1). Earlier entries win, so the lowest index is kept for a
|
|
436
|
+
* duplicated name — smaller on the wire, and it matches what other encoders do.
|
|
437
|
+
*/
|
|
438
|
+
function buildStaticLookup(table, offset) {
|
|
439
|
+
offset = offset || 0;
|
|
440
|
+
var full = {}, name = {};
|
|
441
|
+
for (var i = 0; i < table.length; i++) {
|
|
442
|
+
var key = table[i][0] + '\0' + table[i][1];
|
|
443
|
+
if (!(key in full)) full[key] = i + offset;
|
|
444
|
+
if (!(table[i][0] in name)) name[table[i][0]] = i + offset;
|
|
445
|
+
}
|
|
446
|
+
return { full: full, name: name };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
// ============================================================
|
|
451
|
+
// Text helpers
|
|
452
|
+
//
|
|
453
|
+
// Offered, not imposed — see the note on huffDecode. TextDecoder('utf-8')
|
|
454
|
+
// replaces any non-UTF-8 byte with U+FFFD and loses it; latin-1 is a lossless
|
|
455
|
+
// byte↔char mapping, which is also how node:http surfaces header values. A new
|
|
456
|
+
// decoder should prefer bytesToLatin1; QPACK below stays on UTF-8 because that
|
|
457
|
+
// is what it has always done and this refactor is not the place to change it.
|
|
458
|
+
// ============================================================
|
|
459
|
+
|
|
460
|
+
function bytesToLatin1(bytes) {
|
|
461
|
+
var s = '';
|
|
462
|
+
// Chunked to stay clear of the argument-count limit on very long values.
|
|
463
|
+
for (var i = 0; i < bytes.length; i += 4096) {
|
|
464
|
+
s += String.fromCharCode.apply(null, Array.prototype.slice.call(bytes, i, i + 4096));
|
|
465
|
+
}
|
|
466
|
+
return s;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function _utf8(str) {
|
|
470
|
+
return new TextEncoder().encode(str);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
// ============================================================
|
|
475
|
+
// Dynamic table entry size
|
|
476
|
+
//
|
|
477
|
+
// RFC 7541 §4.1 and RFC 9204 §3.2.1 define it identically: the name, the
|
|
478
|
+
// value, and 32 bytes of assumed per-entry overhead. The constant stands in
|
|
479
|
+
// for the bookkeeping a real implementation carries, and BOTH PEERS MUST
|
|
480
|
+
// AGREE ON IT — a table sized any other way drifts apart from the peer's and
|
|
481
|
+
// every index after the first eviction resolves to the wrong field.
|
|
482
|
+
//
|
|
483
|
+
// In bytes, not characters. Sizing by string length overflows the table on
|
|
484
|
+
// any non-ASCII header value, and does so silently.
|
|
485
|
+
// ============================================================
|
|
486
|
+
|
|
487
|
+
var ENTRY_OVERHEAD = 32;
|
|
488
|
+
|
|
489
|
+
function entrySize(name, value) {
|
|
490
|
+
return Buffer.byteLength(name, 'utf8') + Buffer.byteLength(value, 'utf8') + ENTRY_OVERHEAD;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
export {
|
|
495
|
+
ENTRY_OVERHEAD,
|
|
496
|
+
entrySize,
|
|
497
|
+
// Huffman
|
|
498
|
+
HUFF_CODES,
|
|
499
|
+
HUFF_BITS,
|
|
500
|
+
HUFF_EOS,
|
|
501
|
+
huffDecode,
|
|
502
|
+
huffEncode,
|
|
503
|
+
huffEncodedLength,
|
|
504
|
+
|
|
505
|
+
// Prefixed integers
|
|
506
|
+
readPrefixedInt,
|
|
507
|
+
encodePrefixedInt,
|
|
508
|
+
|
|
509
|
+
// String literals
|
|
510
|
+
decodeStringLiteral,
|
|
511
|
+
encodeStringLiteral,
|
|
512
|
+
|
|
513
|
+
// Static tables
|
|
514
|
+
HPACK_STATIC,
|
|
515
|
+
QPACK_STATIC,
|
|
516
|
+
buildStaticLookup,
|
|
517
|
+
|
|
518
|
+
// Text
|
|
519
|
+
bytesToLatin1
|
|
520
|
+
};
|