tunnelfetch 1.6.3 → 1.6.5
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 +14 -4
- package/README.zh-CN.md +10 -3
- package/package.json +1 -1
- package/src/client.js +3 -0
- package/src/http2/connection.js +9 -2
- package/src/http2/frames.js +34 -4
- package/src/profiles.js +10 -7
- package/types/http2/connection.d.ts +1 -0
- package/types/http2/frames.d.ts +17 -3
- package/types/profiles.d.ts +5 -0
package/README.md
CHANGED
|
@@ -434,10 +434,20 @@ pinned in `test/tls/fingerprint.test.js` and `test/http2/fingerprint.test.js`.
|
|
|
434
434
|
**The default `SETTINGS` values are this package's, not curl's, and that is a deliberate trade.**
|
|
435
435
|
`profiles.curl` carries curl 8.21.0's real `INITIAL_WINDOW_SIZE` of **64 KiB**, captured and pinned
|
|
436
436
|
in `test/tls/_captured-h2.js`. The connection default without a profile is **10 MiB**, which is what
|
|
437
|
-
curl 8.7.1 sent and what this package keeps
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
437
|
+
curl 8.7.1 sent and what this package keeps: a 64 KiB stream window means one `WINDOW_UPDATE` per
|
|
438
|
+
32 KiB consumed instead of one per 5 MiB.
|
|
439
|
+
|
|
440
|
+
**Measured, on a 8.7 MB body over h2 against a real origin, both windows interleaved in one
|
|
441
|
+
isolate: the 64 KiB window costs about +1.4 ms of CPU per decompressed MB** — roughly **6–7%** on
|
|
442
|
+
top of the ~21.7 ms/MB this package spends moving a large body. That is the price of the accurate
|
|
443
|
+
fingerprint, and for most callers it is worth paying; if you are moving large bodies and do not need
|
|
444
|
+
to look like curl, set `http2Settings` yourself.
|
|
445
|
+
|
|
446
|
+
Two cautions on that number. The minimum-of-samples rule this document recommends elsewhere **fails
|
|
447
|
+
here**: with unequal sample counts and a lossy origin the minima moved between +2 ms and +13 ms
|
|
448
|
+
across sweeps, because the arm with more samples gets a lower minimum for free. The figure above is
|
|
449
|
+
p25 and median, which agreed with each other and with the arithmetic — 8.7 MB at a 32 KiB replenish
|
|
450
|
+
threshold is ~276 extra frames, and ~276 × ~47 µs is ~13 ms.
|
|
441
451
|
|
|
442
452
|
Until 1.6.2 the profile carried curl **8.7.1's** window while presenting curl **8.21.0's**
|
|
443
453
|
ClientHello: one named client, two source versions, and a split identity that only a capture could
|
package/README.zh-CN.md
CHANGED
|
@@ -341,9 +341,16 @@ curl(8.7.1 / nghttp2),照线上抓包原样复刻。这么做是实证需
|
|
|
341
341
|
|
|
342
342
|
**默认的 `SETTINGS` 取值是本包自己的,不是 curl 的,这是一个有意的取舍。** `profiles.curl` 带的是
|
|
343
343
|
curl 8.21.0 实测的 `INITIAL_WINDOW_SIZE` = **64 KiB**,抓包固化在 `test/tls/_captured-h2.js`。而不加
|
|
344
|
-
profile 时连接的默认值是 **10 MiB**——那是 curl 8.7.1
|
|
345
|
-
|
|
346
|
-
|
|
344
|
+
profile 时连接的默认值是 **10 MiB**——那是 curl 8.7.1 发的值,本包保留它:64 KiB 的流窗口意味着每消费
|
|
345
|
+
32 KiB 就发一次 `WINDOW_UPDATE`,而不是每 5 MiB 一次。
|
|
346
|
+
|
|
347
|
+
**实测:8.7 MB 的 body、走 h2、打真实源站、两档窗口在同一 isolate 里交错——64 KiB 窗口每解压 MB 多花约
|
|
348
|
+
1.4 ms CPU**,相对本包搬大 body 的约 21.7 ms/MB 是**多 6–7%**。这就是准确指纹的价钱,对多数调用方值得付;
|
|
349
|
+
如果你要搬大 body 又不需要长得像 curl,自己设 `http2Settings`。
|
|
350
|
+
|
|
351
|
+
这个数字有两点要说明。本文档在别处推荐的"取最小值"规则**在这里失效了**:两档样本数不等、源站又有丢失,最小值
|
|
352
|
+
在两轮扫描之间从 +2 ms 跳到 +13 ms——样本多的那一档白得一个更低的最小值。上面的数取自 p25 和中位数,两者一致,
|
|
353
|
+
并且和算术对得上:8.7 MB 在 32 KiB 的补充阈值下约是 276 个额外帧,276 × 约 47 µs 约等于 13 ms。
|
|
347
354
|
|
|
348
355
|
1.6.2 之前,这个 profile 带着 curl **8.7.1** 的窗口,却呈现 curl **8.21.0** 的 ClientHello:同一个具名
|
|
349
356
|
客户端,两个来源版本——一个只有抓包才能发现的身份分裂,因为两半各自都真的属于某个 curl。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.5",
|
|
4
4
|
"description": "A fetch-shaped HTTP client that can route through HTTP CONNECT / HTTPS / SOCKS5 proxies on runtimes with only raw TCP, such as Cloudflare Workers. Implements TLS in userland because the runtime cannot verify a tunnelled peer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"fetch",
|
package/src/client.js
CHANGED
|
@@ -516,6 +516,9 @@ function registerHttp2(client, key, conn) {
|
|
|
516
516
|
...(client.options.http2HpackIndexing
|
|
517
517
|
? { hpackIndexing: client.options.http2HpackIndexing }
|
|
518
518
|
: {}),
|
|
519
|
+
...(client.options.http2HeadersPriority
|
|
520
|
+
? { headersPriority: client.options.http2HeadersPriority }
|
|
521
|
+
: {}),
|
|
519
522
|
onClose: () => {
|
|
520
523
|
client._h2conns.delete(h2);
|
|
521
524
|
// Only drop the keyed entry if it is still this connection; a newer one may have replaced it.
|
package/src/http2/connection.js
CHANGED
|
@@ -266,6 +266,9 @@ export class Http2Connection {
|
|
|
266
266
|
// fields go into the HPACK dynamic table. Both default to curl's, both captured off the wire.
|
|
267
267
|
this._pseudoHeaderOrder = opts.pseudoHeaderOrder ?? null;
|
|
268
268
|
this._hpackIndexing = opts.hpackIndexing ?? null;
|
|
269
|
+
// Whether to set PRIORITY on the request HEADERS, and with what. null means do not set it,
|
|
270
|
+
// which is curl's behaviour and stays the default.
|
|
271
|
+
this._headersPriority = opts.headersPriority ?? null;
|
|
269
272
|
this._expectFirstSettings = true;
|
|
270
273
|
|
|
271
274
|
this._fatal = null; // set once; rejects every stream and every future request
|
|
@@ -449,14 +452,18 @@ export class Http2Connection {
|
|
|
449
452
|
* run is one write, so no other frame can interleave it (RFC 9113 s6.10). */
|
|
450
453
|
_sendHeaderBlock(streamId, block, endStream) {
|
|
451
454
|
const max = this._peerMaxFrameSize;
|
|
455
|
+
// Priority rides the opening HEADERS and nothing else: a CONTINUATION has no flags but
|
|
456
|
+
// END_HEADERS (RFC 9113 s6.10), so putting it anywhere else would be malformed rather than
|
|
457
|
+
// merely wrong-looking.
|
|
458
|
+
const priority = this._headersPriority;
|
|
452
459
|
if (block.length <= max) {
|
|
453
|
-
this._write(headersFrame(streamId, block, { endStream, endHeaders: true }));
|
|
460
|
+
this._write(headersFrame(streamId, block, { endStream, endHeaders: true, priority }));
|
|
454
461
|
return;
|
|
455
462
|
}
|
|
456
463
|
const frames = [];
|
|
457
464
|
let o = 0;
|
|
458
465
|
const first = block.subarray(0, max);
|
|
459
|
-
frames.push(headersFrame(streamId, first, { endStream, endHeaders: false }));
|
|
466
|
+
frames.push(headersFrame(streamId, first, { endStream, endHeaders: false, priority }));
|
|
460
467
|
o = max;
|
|
461
468
|
while (o < block.length) {
|
|
462
469
|
const chunk = block.subarray(o, Math.min(o + max, block.length));
|
package/src/http2/frames.js
CHANGED
|
@@ -152,13 +152,43 @@ export function goawayFrame(lastStreamId, errorCode, debug = EMPTY) {
|
|
|
152
152
|
return serializeFrame(FRAME.GOAWAY, 0, 0, payload);
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
-
/**
|
|
156
|
-
*
|
|
157
|
-
|
|
155
|
+
/**
|
|
156
|
+
* A HEADERS frame carrying a full (already-fragmented-if-needed) block. Never PADDED.
|
|
157
|
+
*
|
|
158
|
+
* PRIORITY is emitted only when `priority` is supplied, because whether a client sends it is part
|
|
159
|
+
* of its identity: curl does not, Chromium does — flags 0x25 with `80 00 00 00 ff`, captured in
|
|
160
|
+
* test/tls/_captured-h2.js. RFC 9113 s5.3.2 deprecates the mechanism and this package ignores
|
|
161
|
+
* every PRIORITY frame it receives, but a frame-level fingerprinter reads whether the flag is set,
|
|
162
|
+
* so refusing to emit it would make the Chromium identity wrong in a way nothing else could fix.
|
|
163
|
+
*
|
|
164
|
+
* The five bytes are exclusive (1 bit) + stream dependency (31) + weight (8). The weight is sent
|
|
165
|
+
* as-is: RFC 7540 s6.3 defines the wire byte as weight-minus-one, so Chromium's 255 is a weight of
|
|
166
|
+
* 256, and passing the byte through keeps this function free of an off-by-one nobody would see.
|
|
167
|
+
*
|
|
168
|
+
* @param {{ exclusive?: boolean, streamDependency?: number, weight: number } | null} [opts.priority]
|
|
169
|
+
*/
|
|
170
|
+
export function headersFrame(
|
|
171
|
+
streamId,
|
|
172
|
+
block,
|
|
173
|
+
{ endStream = false, endHeaders = true, priority = null } = {},
|
|
174
|
+
) {
|
|
158
175
|
let flags = 0;
|
|
159
176
|
if (endStream) flags |= FLAG.END_STREAM;
|
|
160
177
|
if (endHeaders) flags |= FLAG.END_HEADERS;
|
|
161
|
-
return serializeFrame(FRAME.HEADERS, flags, streamId, block);
|
|
178
|
+
if (!priority) return serializeFrame(FRAME.HEADERS, flags, streamId, block);
|
|
179
|
+
|
|
180
|
+
flags |= FLAG.PRIORITY;
|
|
181
|
+
const dep = priority.streamDependency ?? 0;
|
|
182
|
+
const head = new Uint8Array(5);
|
|
183
|
+
head[0] = ((dep >>> 24) & 0x7f) | (priority.exclusive ? 0x80 : 0);
|
|
184
|
+
head[1] = (dep >>> 16) & 0xff;
|
|
185
|
+
head[2] = (dep >>> 8) & 0xff;
|
|
186
|
+
head[3] = dep & 0xff;
|
|
187
|
+
head[4] = priority.weight & 0xff;
|
|
188
|
+
const payload = new Uint8Array(5 + block.length);
|
|
189
|
+
payload.set(head, 0);
|
|
190
|
+
payload.set(block, 5);
|
|
191
|
+
return serializeFrame(FRAME.HEADERS, flags, streamId, payload);
|
|
162
192
|
}
|
|
163
193
|
|
|
164
194
|
/** A CONTINUATION frame (RFC 9113 s6.10), for a header block that overflows one frame. */
|
package/src/profiles.js
CHANGED
|
@@ -117,12 +117,15 @@ export const chrome = Object.freeze({
|
|
|
117
117
|
// have been right, which is not a reason to have guessed. It is declared explicitly now so the
|
|
118
118
|
// identity states its own value instead of inheriting one by accident.
|
|
119
119
|
http2HpackIndexing: Object.freeze({ ':path': 'without' }),
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
120
|
+
// Chromium sets PRIORITY on its request HEADERS (flags 0x25) and carries the five deprecated
|
|
121
|
+
// priority bytes: `80 00 00 00 ff`, i.e. exclusive, dependency 0, weight byte 255. Captured, in
|
|
122
|
+
// test/tls/_captured-h2.js. 1.6.2 found the difference and documented it; 1.6.5 emits it.
|
|
123
|
+
//
|
|
124
|
+
// RFC 9113 s5.3.2 deprecates the mechanism and this package still IGNORES every PRIORITY frame it
|
|
125
|
+
// receives — sending it is a statement about identity, not a request to be prioritised, and those
|
|
126
|
+
// are different things. curl sets no priority, so `profiles.curl` leaves this unset and the
|
|
127
|
+
// default stays "do not send".
|
|
128
|
+
http2HeadersPriority: Object.freeze({ exclusive: true, streamDependency: 0, weight: 255 }),
|
|
126
129
|
//
|
|
127
130
|
// The connection window immediately above was DEAD until 1.6.1 — declared here, copied by
|
|
128
131
|
// nothing, passed by nothing, and read under a different name — so every connection using this
|
|
@@ -211,7 +214,7 @@ export function applyProfile(options) {
|
|
|
211
214
|
// was missing, which made it dead config: the chrome profile declared Chromium's ~15 MiB window
|
|
212
215
|
// and every chrome connection sent curl's 1000 MiB one.
|
|
213
216
|
for (const key of ['headerOrder', 'http2Settings', 'http2ConnectionWindow',
|
|
214
|
-
'http2PseudoHeaderOrder', 'http2HpackIndexing']) {
|
|
217
|
+
'http2PseudoHeaderOrder', 'http2HpackIndexing', 'http2HeadersPriority']) {
|
|
215
218
|
if (options[key] === undefined && p[key] != null) out[key] = p[key];
|
|
216
219
|
}
|
|
217
220
|
// Profile headers are DEFAULTS: a request that sets its own User-Agent keeps it. They are folded
|
|
@@ -79,6 +79,7 @@ export class Http2Connection {
|
|
|
79
79
|
_settingsFlight: Array<[number, number]> | null;
|
|
80
80
|
_pseudoHeaderOrder: string[] | null;
|
|
81
81
|
_hpackIndexing: Record<string, "without" | "incremental" | "never"> | null;
|
|
82
|
+
_headersPriority: any;
|
|
82
83
|
_expectFirstSettings: boolean;
|
|
83
84
|
_fatal: any;
|
|
84
85
|
_goaway: {
|
package/types/http2/frames.d.ts
CHANGED
|
@@ -42,11 +42,25 @@ export function rstStreamFrame(streamId: any, errorCode: any): Uint8Array<ArrayB
|
|
|
42
42
|
export function pingFrame(opaque: any, ack?: boolean): Uint8Array<ArrayBufferLike>;
|
|
43
43
|
/** A GOAWAY frame (RFC 9113 s6.8). */
|
|
44
44
|
export function goawayFrame(lastStreamId: any, errorCode: any, debug?: Uint8Array<ArrayBuffer>): Uint8Array<ArrayBufferLike>;
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
47
|
-
|
|
45
|
+
/**
|
|
46
|
+
* A HEADERS frame carrying a full (already-fragmented-if-needed) block. Never PADDED.
|
|
47
|
+
*
|
|
48
|
+
* PRIORITY is emitted only when `priority` is supplied, because whether a client sends it is part
|
|
49
|
+
* of its identity: curl does not, Chromium does — flags 0x25 with `80 00 00 00 ff`, captured in
|
|
50
|
+
* test/tls/_captured-h2.js. RFC 9113 s5.3.2 deprecates the mechanism and this package ignores
|
|
51
|
+
* every PRIORITY frame it receives, but a frame-level fingerprinter reads whether the flag is set,
|
|
52
|
+
* so refusing to emit it would make the Chromium identity wrong in a way nothing else could fix.
|
|
53
|
+
*
|
|
54
|
+
* The five bytes are exclusive (1 bit) + stream dependency (31) + weight (8). The weight is sent
|
|
55
|
+
* as-is: RFC 7540 s6.3 defines the wire byte as weight-minus-one, so Chromium's 255 is a weight of
|
|
56
|
+
* 256, and passing the byte through keeps this function free of an off-by-one nobody would see.
|
|
57
|
+
*
|
|
58
|
+
* @param {{ exclusive?: boolean, streamDependency?: number, weight: number } | null} [opts.priority]
|
|
59
|
+
*/
|
|
60
|
+
export function headersFrame(streamId: any, block: any, { endStream, endHeaders, priority }?: {
|
|
48
61
|
endStream?: boolean | undefined;
|
|
49
62
|
endHeaders?: boolean | undefined;
|
|
63
|
+
priority?: null | undefined;
|
|
50
64
|
}): Uint8Array<ArrayBufferLike>;
|
|
51
65
|
/** A CONTINUATION frame (RFC 9113 s6.10), for a header block that overflows one frame. */
|
|
52
66
|
export function continuationFrame(streamId: any, block: any, endHeaders: any): Uint8Array<ArrayBufferLike>;
|
package/types/profiles.d.ts
CHANGED
|
@@ -74,6 +74,11 @@ export const chrome: Readonly<{
|
|
|
74
74
|
http2HpackIndexing: Readonly<{
|
|
75
75
|
':path': "without";
|
|
76
76
|
}>;
|
|
77
|
+
http2HeadersPriority: Readonly<{
|
|
78
|
+
exclusive: true;
|
|
79
|
+
streamDependency: 0;
|
|
80
|
+
weight: 255;
|
|
81
|
+
}>;
|
|
77
82
|
http2PseudoHeaderOrder: readonly string[];
|
|
78
83
|
headerOrder: readonly string[];
|
|
79
84
|
headers: readonly string[][];
|