tunnelfetch 1.0.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.
Files changed (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,420 @@
1
+ // HPACK (RFC 7541): header compression for HTTP/2.
2
+ //
3
+ // HPACK is a hostile-input parser and is treated as one throughout. The dynamic table is a
4
+ // documented denial-of-service surface (RFC 7541 s7.1, s7.3): a peer can try to make a decoder
5
+ // allocate unbounded memory, reference a table slot that does not exist, or blow up a small
6
+ // header block into a huge header list. Every one of those is refused here rather than absorbed:
7
+ //
8
+ // * the dynamic table is bounded by the size we advertised in SETTINGS (never the peer's wish),
9
+ // and an index outside the valid range is a typed error naming the index and the bound;
10
+ // * the decoded header LIST is bounded independently, because a bounded table still permits a
11
+ // bomb built from references, and a length that disagrees with the buffer is fatal;
12
+ // * a decoded integer is bounded so a run of continuation bytes cannot spin or overflow;
13
+ // * Huffman decoding (huffman.js) rejects invalid padding, so a "valid" block with junk tail
14
+ // bits does not decode to something a stricter peer would read differently.
15
+ //
16
+ // The encoder is deliberately STATELESS: it references the static table and emits everything else
17
+ // as a literal. It never reads its own dynamic table, so its output does not depend on request
18
+ // history and is trivially reproducible — which is what lets the offline suite pin it byte for
19
+ // byte, and what lets it match curl's first-request encoding exactly. It still emits the
20
+ // incremental-indexing opcode where curl does (so the peer's table state, and thus the wire
21
+ // fingerprint, matches), it simply never depends on the entries that creates.
22
+
23
+ import { Http2Error, LimitError, codes } from '../errors.js';
24
+ import { concat, utf8 } from '../util/bytes.js';
25
+ import { huffmanDecode, huffmanEncode, huffmanEncodedLength } from './huffman.js';
26
+
27
+ const decoder = new TextDecoder('utf-8', { fatal: false });
28
+
29
+ /** RFC 7541 Appendix A. Index 0 is a placeholder so table[i] is the RFC's entry i (1-based). */
30
+ const STATIC_TABLE = [
31
+ ['', ''],
32
+ [':authority', ''],
33
+ [':method', 'GET'],
34
+ [':method', 'POST'],
35
+ [':path', '/'],
36
+ [':path', '/index.html'],
37
+ [':scheme', 'http'],
38
+ [':scheme', 'https'],
39
+ [':status', '200'],
40
+ [':status', '204'],
41
+ [':status', '206'],
42
+ [':status', '304'],
43
+ [':status', '400'],
44
+ [':status', '404'],
45
+ [':status', '500'],
46
+ ['accept-charset', ''],
47
+ ['accept-encoding', 'gzip, deflate'],
48
+ ['accept-language', ''],
49
+ ['accept-ranges', ''],
50
+ ['accept', ''],
51
+ ['access-control-allow-origin', ''],
52
+ ['age', ''],
53
+ ['allow', ''],
54
+ ['authorization', ''],
55
+ ['cache-control', ''],
56
+ ['content-disposition', ''],
57
+ ['content-encoding', ''],
58
+ ['content-language', ''],
59
+ ['content-length', ''],
60
+ ['content-location', ''],
61
+ ['content-range', ''],
62
+ ['content-type', ''],
63
+ ['cookie', ''],
64
+ ['date', ''],
65
+ ['etag', ''],
66
+ ['expect', ''],
67
+ ['expires', ''],
68
+ ['from', ''],
69
+ ['host', ''],
70
+ ['if-match', ''],
71
+ ['if-modified-since', ''],
72
+ ['if-none-match', ''],
73
+ ['if-range', ''],
74
+ ['if-unmodified-since', ''],
75
+ ['last-modified', ''],
76
+ ['link', ''],
77
+ ['location', ''],
78
+ ['max-forwards', ''],
79
+ ['proxy-authenticate', ''],
80
+ ['proxy-authorization', ''],
81
+ ['range', ''],
82
+ ['referer', ''],
83
+ ['refresh', ''],
84
+ ['retry-after', ''],
85
+ ['server', ''],
86
+ ['set-cookie', ''],
87
+ ['strict-transport-security', ''],
88
+ ['transfer-encoding', ''],
89
+ ['user-agent', ''],
90
+ ['vary', ''],
91
+ ['via', ''],
92
+ ['www-authenticate', ''],
93
+ ];
94
+ const STATIC_COUNT = STATIC_TABLE.length - 1; // 61
95
+
96
+ /** The overhead RFC 7541 s4.1 charges every dynamic entry, on top of its name and value octets. */
97
+ const ENTRY_OVERHEAD = 32;
98
+
99
+ /** Default SETTINGS_HEADER_TABLE_SIZE (RFC 9113 s6.5.2): the size we advertise, so the size the
100
+ * peer's encoder may fill in our decoder. A size update above this is a COMPRESSION_ERROR. */
101
+ export const DEFAULT_HEADER_TABLE_SIZE = 4096;
102
+
103
+ // A single integer with many continuation bytes is a spin/overflow attempt; 6 bytes past the
104
+ // prefix already covers any length this client will ever see (2^35), so more is refused.
105
+ const MAX_INTEGER_CONTINUATION_BYTES = 6;
106
+
107
+ // ---------------------------------------------------------------------- integer / string coding
108
+
109
+ /**
110
+ * Decode an HPACK variable-length integer (RFC 7541 s5.1) with an `n`-bit prefix.
111
+ * @param {Uint8Array} buf
112
+ * @param {number} pos index of the prefix byte
113
+ * @param {number} n prefix bit width (1..8)
114
+ * @returns {{ value: number, pos: number }}
115
+ */
116
+ function decodeInteger(buf, pos, n) {
117
+ const max = (1 << n) - 1;
118
+ if (pos >= buf.length) {
119
+ throw new Http2Error(codes.HTTP2_COMPRESSION, 'truncated HPACK integer prefix');
120
+ }
121
+ let value = buf[pos] & max;
122
+ pos++;
123
+ if (value < max) return { value, pos };
124
+ let m = 0;
125
+ let bytes = 0;
126
+ for (;;) {
127
+ if (pos >= buf.length) {
128
+ throw new Http2Error(codes.HTTP2_COMPRESSION, 'truncated HPACK integer continuation');
129
+ }
130
+ if (++bytes > MAX_INTEGER_CONTINUATION_BYTES) {
131
+ throw new Http2Error(
132
+ codes.HTTP2_COMPRESSION,
133
+ `HPACK integer has more than ${MAX_INTEGER_CONTINUATION_BYTES} continuation bytes`,
134
+ );
135
+ }
136
+ const b = buf[pos++];
137
+ value += (b & 0x7f) * 2 ** m;
138
+ if (!Number.isSafeInteger(value)) {
139
+ throw new Http2Error(codes.HTTP2_COMPRESSION, 'HPACK integer overflows a safe integer');
140
+ }
141
+ if ((b & 0x80) === 0) break;
142
+ m += 7;
143
+ }
144
+ return { value, pos };
145
+ }
146
+
147
+ /**
148
+ * Decode an HPACK string literal (RFC 7541 s5.2): a Huffman flag, a length, then the octets.
149
+ * @param {Uint8Array} buf
150
+ * @param {number} pos
151
+ * @returns {{ bytes: Uint8Array, pos: number }}
152
+ */
153
+ function decodeString(buf, pos) {
154
+ if (pos >= buf.length) {
155
+ throw new Http2Error(codes.HTTP2_COMPRESSION, 'truncated HPACK string');
156
+ }
157
+ const huffman = (buf[pos] & 0x80) !== 0;
158
+ const { value: len, pos: p } = decodeInteger(buf, pos, 7);
159
+ if (p + len > buf.length) {
160
+ throw new Http2Error(
161
+ codes.HTTP2_COMPRESSION,
162
+ `HPACK string length ${len} runs past the end of the header block`,
163
+ { length: len, available: buf.length - p },
164
+ );
165
+ }
166
+ const raw = buf.subarray(p, p + len);
167
+ return { bytes: huffman ? huffmanDecode(raw) : raw.slice(), pos: p + len };
168
+ }
169
+
170
+ /** Encode an HPACK integer with an `n`-bit prefix; `prefixBits` are the high bits of the first byte. */
171
+ function encodeInteger(value, n, prefixBits) {
172
+ const max = (1 << n) - 1;
173
+ if (value < max) return Uint8Array.of(prefixBits | value);
174
+ const out = [prefixBits | max];
175
+ let v = value - max;
176
+ while (v >= 0x80) {
177
+ out.push((v & 0x7f) | 0x80);
178
+ v = Math.floor(v / 128);
179
+ }
180
+ out.push(v);
181
+ return Uint8Array.from(out);
182
+ }
183
+
184
+ /** Encode an HPACK string literal, Huffman-coding it only when that is strictly shorter. */
185
+ function encodeString(bytes) {
186
+ if (huffmanEncodedLength(bytes) < bytes.length) {
187
+ const packed = huffmanEncode(bytes);
188
+ return concat([encodeInteger(packed.length, 7, 0x80), packed]);
189
+ }
190
+ return concat([encodeInteger(bytes.length, 7, 0x00), bytes]);
191
+ }
192
+
193
+ // ---------------------------------------------------------------------- decoder
194
+
195
+ /**
196
+ * @typedef {object} HpackDecoderOptions
197
+ * @property {number} [maxTableSize] the SETTINGS_HEADER_TABLE_SIZE we advertised; the dynamic
198
+ * table may not exceed it and a size update above it is fatal. Default 4096.
199
+ * @property {number} [maxHeaderListSize] fail-closed cap on the decoded header list (sum of
200
+ * name+value+32 per field). Default 262144. We do not advertise this to the peer, so it is a
201
+ * self-protection bound, not a promise; over it is a LimitError.
202
+ */
203
+
204
+ /**
205
+ * A stateful HPACK decoder. One per connection per direction: the dynamic table is shared state
206
+ * across every header block on the connection, so a single decoder instance must live as long as
207
+ * the connection does.
208
+ */
209
+ export class HpackDecoder {
210
+ /** @param {HpackDecoderOptions} [opts] */
211
+ constructor({ maxTableSize = DEFAULT_HEADER_TABLE_SIZE, maxHeaderListSize = 262144 } = {}) {
212
+ /** @type {Array<[string, string, number]>} name, value, entry-size — newest at index 0 */
213
+ this._dynamic = [];
214
+ this._size = 0;
215
+ this._maxSize = maxTableSize;
216
+ this._limit = maxTableSize; // the hard ceiling a size update may not exceed
217
+ this._maxHeaderListSize = maxHeaderListSize;
218
+ }
219
+
220
+ /** Current number of entries, exposed for tests that assert eviction. */
221
+ get dynamicLength() {
222
+ return this._dynamic.length;
223
+ }
224
+
225
+ /**
226
+ * Look up index `i` (RFC 7541 s2.3.3: 1..STATIC_COUNT is static, above that is dynamic,
227
+ * newest first). Fails closed on 0 and on anything past the end of the dynamic table.
228
+ * @param {number} i
229
+ * @returns {[string, string]}
230
+ */
231
+ _lookup(i) {
232
+ if (i === 0) {
233
+ throw new Http2Error(codes.HTTP2_COMPRESSION, 'HPACK index 0 is not a valid table entry');
234
+ }
235
+ if (i <= STATIC_COUNT) return STATIC_TABLE[i];
236
+ const d = i - STATIC_COUNT - 1;
237
+ if (d >= this._dynamic.length) {
238
+ throw new Http2Error(
239
+ codes.HTTP2_COMPRESSION,
240
+ `HPACK index ${i} is out of range (static 1..${STATIC_COUNT}, ` +
241
+ `dynamic holds ${this._dynamic.length})`,
242
+ { index: i, staticCount: STATIC_COUNT, dynamicLength: this._dynamic.length },
243
+ );
244
+ }
245
+ const [name, value] = this._dynamic[d];
246
+ return [name, value];
247
+ }
248
+
249
+ /** Insert into the dynamic table, evicting oldest entries until it fits (RFC 7541 s4.4). */
250
+ _insert(name, value) {
251
+ const entrySize = name.length + value.length + ENTRY_OVERHEAD;
252
+ // Adding an entry larger than the whole table empties the table and stores nothing.
253
+ while (this._size + entrySize > this._maxSize && this._dynamic.length > 0) {
254
+ this._size -= this._dynamic.pop()[2];
255
+ }
256
+ if (entrySize <= this._maxSize) {
257
+ this._dynamic.unshift([name, value, entrySize]);
258
+ this._size += entrySize;
259
+ }
260
+ }
261
+
262
+ /** Apply a dynamic table size update (RFC 7541 s6.3), refusing one above what we advertised. */
263
+ _resize(newSize) {
264
+ if (newSize > this._limit) {
265
+ throw new Http2Error(
266
+ codes.HTTP2_COMPRESSION,
267
+ `dynamic table size update to ${newSize} exceeds the advertised limit ${this._limit}`,
268
+ { requested: newSize, limit: this._limit },
269
+ );
270
+ }
271
+ this._maxSize = newSize;
272
+ while (this._size > this._maxSize && this._dynamic.length > 0) {
273
+ this._size -= this._dynamic.pop()[2];
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Decode one header block into an ordered list of [name, value] pairs.
279
+ * @param {Uint8Array} block
280
+ * @returns {Array<[string, string]>}
281
+ */
282
+ decode(block) {
283
+ const out = [];
284
+ let listSize = 0;
285
+ let pos = 0;
286
+ // A dynamic table size update is only legal before any header field in the block (s4.2).
287
+ let sawField = false;
288
+ const add = (name, value) => {
289
+ listSize += name.length + value.length + ENTRY_OVERHEAD;
290
+ if (listSize > this._maxHeaderListSize) {
291
+ throw new LimitError(
292
+ codes.HTTP2_HEADER,
293
+ `decoded header list reached ${listSize} bytes, over the ` +
294
+ `${this._maxHeaderListSize} byte limit`,
295
+ { limit: this._maxHeaderListSize },
296
+ );
297
+ }
298
+ out.push([name, value]);
299
+ };
300
+
301
+ while (pos < block.length) {
302
+ const first = block[pos];
303
+ if (first & 0x80) {
304
+ // 6.1 Indexed Header Field.
305
+ const { value: idx, pos: p } = decodeInteger(block, pos, 7);
306
+ pos = p;
307
+ const [name, value] = this._lookup(idx);
308
+ add(name, value);
309
+ sawField = true;
310
+ } else if (first & 0x40) {
311
+ // 6.2.1 Literal with Incremental Indexing.
312
+ ({ pos } = this._literal(block, pos, 6, out.length, add, (n, v) => this._insert(n, v)));
313
+ sawField = true;
314
+ } else if (first & 0x20) {
315
+ // 6.3 Dynamic Table Size Update.
316
+ if (sawField) {
317
+ throw new Http2Error(
318
+ codes.HTTP2_COMPRESSION,
319
+ 'dynamic table size update must precede every header field in the block (RFC 7541 s4.2)',
320
+ );
321
+ }
322
+ const { value: newSize, pos: p } = decodeInteger(block, pos, 5);
323
+ pos = p;
324
+ this._resize(newSize);
325
+ } else {
326
+ // 6.2.2 Literal without Indexing (0x00) and 6.2.3 Literal Never Indexed (0x10) — both
327
+ // 4-bit prefix, neither touches the dynamic table.
328
+ ({ pos } = this._literal(block, pos, 4, out.length, add, null));
329
+ sawField = true;
330
+ }
331
+ }
332
+ return out;
333
+ }
334
+
335
+ /**
336
+ * Shared body of the three literal representations: an optionally indexed name, then a value.
337
+ * @param {(name: string, value: string) => void} add
338
+ * @param {null | ((name: string, value: string) => void)} index insert into the dynamic table, or null
339
+ */
340
+ _literal(block, pos, prefixBits, _fieldIndex, add, index) {
341
+ const { value: nameIdx, pos: p1 } = decodeInteger(block, pos, prefixBits);
342
+ let name;
343
+ let p = p1;
344
+ if (nameIdx === 0) {
345
+ const s = decodeString(block, p);
346
+ name = decoder.decode(s.bytes);
347
+ p = s.pos;
348
+ } else {
349
+ name = this._lookup(nameIdx)[0];
350
+ }
351
+ const vs = decodeString(block, p);
352
+ const value = decoder.decode(vs.bytes);
353
+ add(name, value);
354
+ if (index) index(name, value);
355
+ return { pos: vs.pos };
356
+ }
357
+ }
358
+
359
+ // ---------------------------------------------------------------------- encoder
360
+
361
+ /**
362
+ * Find a static-table entry. Returns `{ index }` for a full name+value match, `{ nameIndex }`
363
+ * for a name-only match, or null. The dynamic table is intentionally not consulted; see the
364
+ * module header for why a stateless encoder is the right trade here.
365
+ */
366
+ function staticLookup(name, value) {
367
+ let nameIndex = 0;
368
+ for (let i = 1; i <= STATIC_COUNT; i++) {
369
+ if (STATIC_TABLE[i][0] === name) {
370
+ if (STATIC_TABLE[i][1] === value) return { index: i };
371
+ if (nameIndex === 0) nameIndex = i;
372
+ }
373
+ }
374
+ return nameIndex ? { nameIndex } : null;
375
+ }
376
+
377
+ /**
378
+ * @typedef {object} HpackField
379
+ * @property {string} name already lowercased by the caller — HPACK does not case-fold, and
380
+ * HTTP/2 forbids uppercase field names on the wire (RFC 9113 s8.2.1)
381
+ * @property {string} value
382
+ * @property {'incremental' | 'without' | 'never'} [indexing] how to represent it when it is not a
383
+ * full static match. Default 'incremental', which is what curl uses for most fields.
384
+ */
385
+
386
+ /**
387
+ * Encode an ordered list of header fields into one HPACK block. Order is preserved exactly, which
388
+ * is load-bearing: HTTP/2 requires all pseudo-headers before regular ones, and the pseudo-header
389
+ * ORDER is part of the client fingerprint the caller is matching.
390
+ *
391
+ * @param {HpackField[]} fields
392
+ * @returns {Uint8Array}
393
+ */
394
+ export function encodeHeaderBlock(fields) {
395
+ const parts = [];
396
+ for (const f of fields) {
397
+ const name = f.name;
398
+ const valueBytes = utf8(f.value);
399
+ const found = staticLookup(name, f.value);
400
+ if (found && found.index !== undefined) {
401
+ // A full match is the same one byte for everyone; curl uses it for :method and :scheme.
402
+ parts.push(encodeInteger(found.index, 7, 0x80));
403
+ continue;
404
+ }
405
+ const indexing = f.indexing ?? 'incremental';
406
+ // prefix pattern and width per representation (RFC 7541 s6.2).
407
+ const [prefixBits, prefixWidth] =
408
+ indexing === 'incremental' ? [0x40, 6] : indexing === 'never' ? [0x10, 4] : [0x00, 4];
409
+ const nameIndex = found ? found.nameIndex : 0;
410
+ if (nameIndex) {
411
+ parts.push(encodeInteger(nameIndex, prefixWidth, prefixBits));
412
+ } else {
413
+ parts.push(encodeInteger(0, prefixWidth, prefixBits), encodeString(utf8(name)));
414
+ }
415
+ parts.push(encodeString(valueBytes));
416
+ }
417
+ return concat(parts);
418
+ }
419
+
420
+ export { STATIC_TABLE, STATIC_COUNT };
@@ -0,0 +1,203 @@
1
+ // HPACK Huffman coding (RFC 7541 s5.2 and Appendix B).
2
+ //
3
+ // The code table is the RFC's verbatim — 256 symbols plus EOS at index 256 — and the decoder is
4
+ // deliberately the paranoid kind, because a Huffman string is attacker-controlled input inside a
5
+ // header block. RFC 7541 s5.2 names three decoding errors and this decoder raises every one:
6
+ //
7
+ // * a decoded EOS symbol (256) is illegal in a string literal,
8
+ // * padding longer than 7 bits is illegal (that many bits could have started another symbol),
9
+ // * padding that is not the most-significant bits of EOS — i.e. not all ones — is illegal.
10
+ //
11
+ // A decoder that shrugged any of those off would let two peers disagree on what a header said,
12
+ // which in HPACK is the same class of ambiguity that request smuggling is in HTTP/1.1.
13
+
14
+ import { Http2Error, codes } from '../errors.js';
15
+
16
+ // RFC 7541 Appendix B. HUFFMAN_CODE[sym] is the code right-aligned in an integer of HUFFMAN_LEN[sym]
17
+ // bits; index 256 is EOS. Every code fits in 30 bits, so plain numbers suffice (no BigInt).
18
+ const HUFFMAN_CODE = [
19
+ 0x1ff8, 0x7fffd8, 0xfffffe2, 0xfffffe3, 0xfffffe4, 0xfffffe5, 0xfffffe6, 0xfffffe7,
20
+ 0xfffffe8, 0xffffea, 0x3ffffffc, 0xfffffe9, 0xfffffea, 0x3ffffffd, 0xfffffeb, 0xfffffec,
21
+ 0xfffffed, 0xfffffee, 0xfffffef, 0xffffff0, 0xffffff1, 0xffffff2, 0x3ffffffe, 0xffffff3,
22
+ 0xffffff4, 0xffffff5, 0xffffff6, 0xffffff7, 0xffffff8, 0xffffff9, 0xffffffa, 0xffffffb,
23
+ 0x14, 0x3f8, 0x3f9, 0xffa, 0x1ff9, 0x15, 0xf8, 0x7fa,
24
+ 0x3fa, 0x3fb, 0xf9, 0x7fb, 0xfa, 0x16, 0x17, 0x18,
25
+ 0x0, 0x1, 0x2, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
26
+ 0x1e, 0x1f, 0x5c, 0xfb, 0x7ffc, 0x20, 0xffb, 0x3fc,
27
+ 0x1ffa, 0x21, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62,
28
+ 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a,
29
+ 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72,
30
+ 0xfc, 0x73, 0xfd, 0x1ffb, 0x7fff0, 0x1ffc, 0x3ffc, 0x22,
31
+ 0x7ffd, 0x3, 0x23, 0x4, 0x24, 0x5, 0x25, 0x26,
32
+ 0x27, 0x6, 0x74, 0x75, 0x28, 0x29, 0x2a, 0x7,
33
+ 0x2b, 0x76, 0x2c, 0x8, 0x9, 0x2d, 0x77, 0x78,
34
+ 0x79, 0x7a, 0x7b, 0x7ffe, 0x7fc, 0x3ffd, 0x1ffd, 0xffffffc,
35
+ 0xfffe6, 0x3fffd2, 0xfffe7, 0xfffe8, 0x3fffd3, 0x3fffd4, 0x3fffd5, 0x7fffd9,
36
+ 0x3fffd6, 0x7fffda, 0x7fffdb, 0x7fffdc, 0x7fffdd, 0x7fffde, 0xffffeb, 0x7fffdf,
37
+ 0xffffec, 0xffffed, 0x3fffd7, 0x7fffe0, 0xffffee, 0x7fffe1, 0x7fffe2, 0x7fffe3,
38
+ 0x7fffe4, 0x1fffdc, 0x3fffd8, 0x7fffe5, 0x3fffd9, 0x7fffe6, 0x7fffe7, 0xffffef,
39
+ 0x3fffda, 0x1fffdd, 0xfffe9, 0x3fffdb, 0x3fffdc, 0x7fffe8, 0x7fffe9, 0x1fffde,
40
+ 0x7fffea, 0x3fffdd, 0x3fffde, 0xfffff0, 0x1fffdf, 0x3fffdf, 0x7fffeb, 0x7fffec,
41
+ 0x1fffe0, 0x1fffe1, 0x3fffe0, 0x1fffe2, 0x7fffed, 0x3fffe1, 0x7fffee, 0x7fffef,
42
+ 0xfffea, 0x3fffe2, 0x3fffe3, 0x3fffe4, 0x7ffff0, 0x3fffe5, 0x3fffe6, 0x7ffff1,
43
+ 0x3ffffe0, 0x3ffffe1, 0xfffeb, 0x7fff1, 0x3fffe7, 0x7ffff2, 0x3fffe8, 0x1ffffec,
44
+ 0x3ffffe2, 0x3ffffe3, 0x3ffffe4, 0x7ffffde, 0x7ffffdf, 0x3ffffe5, 0xfffff1, 0x1ffffed,
45
+ 0x7fff2, 0x1fffe3, 0x3ffffe6, 0x7ffffe0, 0x7ffffe1, 0x3ffffe7, 0x7ffffe2, 0xfffff2,
46
+ 0x1fffe4, 0x1fffe5, 0x3ffffe8, 0x3ffffe9, 0xffffffd, 0x7ffffe3, 0x7ffffe4, 0x7ffffe5,
47
+ 0xfffec, 0xfffff3, 0xfffed, 0x1fffe6, 0x3fffe9, 0x1fffe7, 0x1fffe8, 0x7ffff3,
48
+ 0x3fffea, 0x3fffeb, 0x1ffffee, 0x1ffffef, 0xfffff4, 0xfffff5, 0x3ffffea, 0x7ffff4,
49
+ 0x3ffffeb, 0x7ffffe6, 0x3ffffec, 0x3ffffed, 0x7ffffe7, 0x7ffffe8, 0x7ffffe9, 0x7ffffea,
50
+ 0x7ffffeb, 0xffffffe, 0x7ffffec, 0x7ffffed, 0x7ffffee, 0x7ffffef, 0x7fffff0, 0x3ffffee,
51
+ 0x3fffffff,
52
+ ];
53
+
54
+ const HUFFMAN_LEN = [
55
+ 13, 23, 28, 28, 28, 28, 28, 28, 28, 24, 30, 28, 28, 30, 28, 28,
56
+ 28, 28, 28, 28, 28, 28, 30, 28, 28, 28, 28, 28, 28, 28, 28, 28,
57
+ 6, 10, 10, 12, 13, 6, 8, 11, 10, 10, 8, 11, 8, 6, 6, 6,
58
+ 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 7, 8, 15, 6, 12, 10,
59
+ 13, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,
60
+ 7, 7, 7, 7, 7, 7, 7, 7, 8, 7, 8, 13, 19, 13, 14, 6,
61
+ 15, 5, 6, 5, 6, 5, 6, 6, 6, 5, 7, 7, 6, 6, 6, 5,
62
+ 6, 7, 6, 5, 5, 6, 7, 7, 7, 7, 7, 15, 11, 14, 13, 28,
63
+ 20, 22, 20, 20, 22, 22, 22, 23, 22, 23, 23, 23, 23, 23, 24, 23,
64
+ 24, 24, 22, 23, 24, 23, 23, 23, 23, 21, 22, 23, 22, 23, 23, 24,
65
+ 22, 21, 20, 22, 22, 23, 23, 21, 23, 22, 22, 24, 21, 22, 23, 23,
66
+ 21, 21, 22, 21, 23, 22, 23, 23, 20, 22, 22, 22, 23, 22, 22, 23,
67
+ 26, 26, 20, 19, 22, 23, 22, 25, 26, 26, 26, 27, 27, 26, 24, 25,
68
+ 19, 21, 26, 27, 27, 26, 27, 24, 21, 21, 26, 26, 28, 27, 27, 27,
69
+ 20, 24, 20, 21, 22, 21, 21, 23, 22, 22, 25, 25, 24, 24, 26, 23,
70
+ 26, 27, 26, 26, 27, 27, 27, 27, 27, 28, 27, 27, 27, 27, 27, 26,
71
+ 30,
72
+ ];
73
+
74
+ const EOS = 256;
75
+
76
+ /**
77
+ * Bit trie, flat so decoding allocates nothing. Two child arrays index into themselves; a
78
+ * non-negative `sym[node]` marks a leaf. Built once from the code table at module load.
79
+ */
80
+ const trie = buildTrie();
81
+
82
+ function buildTrie() {
83
+ const left = [-1]; // child on bit 0
84
+ const right = [-1]; // child on bit 1
85
+ const sym = [-1];
86
+ let next = 1;
87
+ for (let s = 0; s < HUFFMAN_CODE.length; s++) {
88
+ const code = HUFFMAN_CODE[s];
89
+ const len = HUFFMAN_LEN[s];
90
+ let node = 0;
91
+ for (let i = len - 1; i >= 0; i--) {
92
+ const bit = (code >>> i) & 1;
93
+ const arr = bit ? right : left;
94
+ if (arr[node] === -1) {
95
+ arr[node] = next;
96
+ left[next] = -1;
97
+ right[next] = -1;
98
+ sym[next] = -1;
99
+ next++;
100
+ }
101
+ node = arr[node];
102
+ }
103
+ sym[node] = s;
104
+ }
105
+ return { left, right, sym };
106
+ }
107
+
108
+ /**
109
+ * The number of bytes `bytes` would occupy Huffman-encoded, so a caller can choose the shorter
110
+ * of literal and Huffman exactly the way nghttp2 does (this is what makes curl leave the accept
111
+ * value un-encoded when its Huffman form is not shorter).
112
+ * @param {Uint8Array} bytes
113
+ * @returns {number}
114
+ */
115
+ export function huffmanEncodedLength(bytes) {
116
+ let bits = 0;
117
+ for (let i = 0; i < bytes.length; i++) bits += HUFFMAN_LEN[bytes[i]];
118
+ return (bits + 7) >> 3;
119
+ }
120
+
121
+ /**
122
+ * Huffman-encode octets, padding the final byte with the most-significant bits of EOS (all ones)
123
+ * per RFC 7541 s5.2.
124
+ * @param {Uint8Array} bytes
125
+ * @returns {Uint8Array}
126
+ */
127
+ export function huffmanEncode(bytes) {
128
+ const out = new Uint8Array(huffmanEncodedLength(bytes));
129
+ let o = 0;
130
+ // Bit accumulator holding `nbits` pending bits (nbits stays < 8 between symbols, so the widest
131
+ // intermediate is < 8 + 30 = 38 bits — comfortably inside a double, no 32-bit shift, no BigInt).
132
+ let acc = 0;
133
+ let nbits = 0;
134
+ for (let i = 0; i < bytes.length; i++) {
135
+ const sym = bytes[i];
136
+ acc = acc * 2 ** HUFFMAN_LEN[sym] + HUFFMAN_CODE[sym];
137
+ nbits += HUFFMAN_LEN[sym];
138
+ while (nbits >= 8) {
139
+ nbits -= 8;
140
+ out[o++] = Math.floor(acc / 2 ** nbits) & 0xff;
141
+ acc %= 2 ** nbits; // drop the bits just emitted so acc always holds exactly `nbits` bits
142
+ }
143
+ }
144
+ if (nbits > 0) {
145
+ // Pad the last byte with EOS's leading bits, which are all ones (RFC 7541 s5.2).
146
+ const pad = 8 - nbits;
147
+ out[o++] = (acc * 2 ** pad + (2 ** pad - 1)) & 0xff;
148
+ }
149
+ return out;
150
+ }
151
+
152
+ /**
153
+ * Huffman-decode octets, failing closed on every error RFC 7541 s5.2 names.
154
+ * @param {Uint8Array} bytes
155
+ * @returns {Uint8Array}
156
+ */
157
+ export function huffmanDecode(bytes) {
158
+ const { left, right, sym } = trie;
159
+ const out = [];
160
+ let node = 0;
161
+ let depth = 0; // bits consumed in the current, still-incomplete symbol
162
+ let allOnes = true; // whether every bit of the current partial symbol has been 1
163
+ for (let i = 0; i < bytes.length; i++) {
164
+ const byte = bytes[i];
165
+ for (let b = 7; b >= 0; b--) {
166
+ const bit = (byte >>> b) & 1;
167
+ node = bit ? right[node] : left[node];
168
+ depth++;
169
+ if (bit === 0) allOnes = false;
170
+ // The trie is a full prefix code, so a walk can never fall off it; node is always valid.
171
+ const s = sym[node];
172
+ if (s !== -1) {
173
+ if (s === EOS) {
174
+ throw new Http2Error(
175
+ codes.HTTP2_COMPRESSION,
176
+ 'Huffman string contains the EOS symbol, which RFC 7541 s5.2 forbids in a literal',
177
+ );
178
+ }
179
+ out.push(s);
180
+ node = 0;
181
+ depth = 0;
182
+ allOnes = true;
183
+ }
184
+ }
185
+ }
186
+ if (depth > 0) {
187
+ // Whatever is left must be valid padding: at most 7 bits, and all ones (the EOS prefix).
188
+ if (depth > 7) {
189
+ throw new Http2Error(
190
+ codes.HTTP2_COMPRESSION,
191
+ `Huffman padding is ${depth} bits, over the 7-bit maximum RFC 7541 s5.2 allows`,
192
+ { padBits: depth },
193
+ );
194
+ }
195
+ if (!allOnes) {
196
+ throw new Http2Error(
197
+ codes.HTTP2_COMPRESSION,
198
+ 'Huffman padding is not the most-significant bits of EOS (a non-one bit appears in it)',
199
+ );
200
+ }
201
+ }
202
+ return Uint8Array.from(out);
203
+ }
@@ -0,0 +1,21 @@
1
+ // HTTP/2 (RFC 9113) over a byte duplex: frame layer, HPACK (RFC 7541), and the multiplexing
2
+ // connection engine. This barrel is the layer's public face; client.js drives it, and the pieces
3
+ // are individually importable for testing.
4
+ //
5
+ // The whole reason this exists is access, not speed: some sites treat HTTP/1.1 as a bot signal and
6
+ // let curl's HTTP/2 through, so the client posture here is matched to curl's on the wire. It is not
7
+ // a performance win on a CPU-billed runtime — HPACK is work HTTP/1.1 does not do. See the README.
8
+
9
+ export { Http2Connection, Http2Retryable, buildRequestFields } from './connection.js';
10
+ export { HpackDecoder, encodeHeaderBlock } from './hpack.js';
11
+ export { huffmanDecode, huffmanEncode } from './huffman.js';
12
+ export {
13
+ readFrame,
14
+ serializeFrame,
15
+ settingsFrame,
16
+ windowUpdateFrame,
17
+ dataFrame,
18
+ headersFrame,
19
+ parseSettings,
20
+ } from './frames.js';
21
+ export { ALPN_H2, ALPN_HTTP11 } from './constants.js';
package/src/index.js ADDED
@@ -0,0 +1,46 @@
1
+ // tunnelfetch — a fetch-shaped HTTP client that can route through a proxy on runtimes that only
2
+ // expose raw TCP.
3
+ //
4
+ // Why this exists, in one paragraph: on Cloudflare Workers `fetch()` has no proxy option,
5
+ // `node:net`/`node:tls` are built on the same socket API and inherit its limits, and the socket
6
+ // API's own TLS verifies the certificate against the hostname passed to `connect()` — which,
7
+ // inside a CONNECT or SOCKS5 tunnel, is the proxy rather than the origin. It also exposes no peer
8
+ // certificate, so the check cannot be redone afterwards. Measured on the edge: a tunnelled
9
+ // `startTls()` fails closed, which is the right failure but leaves no route at all. The only
10
+ // remaining path is to speak TLS ourselves, which is what this package does.
11
+ //
12
+ // Nothing here installs itself. `install()` is explicit and returns its own undo.
13
+
14
+ export { Client, createFetch, install } from './client.js';
15
+ export { warmup } from './warmup.js';
16
+ export { ConnectionPool, poolKey } from './pool.js';
17
+ export { openConnection, targetFromUrl, nativeFetchCanServe } from './transport.js';
18
+ export { openTunnel, parseProxy } from './proxy/index.js';
19
+ export { CookieJar } from './client/cookies.js';
20
+ export { verifyChain, rootStoreProvenance } from './trust/index.js';
21
+
22
+ export {
23
+ TunnelFetchError,
24
+ ProxyError,
25
+ HttpError,
26
+ TlsError,
27
+ TlsUnsupportedError,
28
+ Http2Error,
29
+ CertificateError,
30
+ TimeoutError,
31
+ LimitError,
32
+ ConfigError,
33
+ codes,
34
+ } from './errors.js';
35
+
36
+ import { createFetch } from './client.js';
37
+
38
+ /**
39
+ * A ready-made fetch with default options: no proxy, system trust, one connection per call.
40
+ * Equivalent to httpx.get() versus httpx.Client() — reach for `new Client()` when connection
41
+ * reuse or a cookie jar matters, which for anything crawl-shaped it does.
42
+ *
43
+ * A `connect` function must still be supplied per call (or via `createFetch`) for any request the
44
+ * platform's own fetch cannot serve; see the README for why that is not defaulted.
45
+ */
46
+ export const fetch = createFetch();