tunnelfetch 1.6.1 → 1.6.3
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 +13 -1
- package/README.zh-CN.md +10 -1
- package/package.json +1 -1
- package/src/http2/connection.js +15 -1
- package/src/profiles.js +31 -11
- package/types/profiles.d.ts +3 -0
package/README.md
CHANGED
|
@@ -426,11 +426,23 @@ pinned in `test/tls/fingerprint.test.js` and `test/http2/fingerprint.test.js`.
|
|
|
426
426
|
| Supported groups | `x25519, secp256r1, secp384r1, secp521r1` | `tls.groups` |
|
|
427
427
|
| Signature algorithms | ECDSA and RSA-PSS/PKCS#1 over SHA-256/384/512 | `tls.sigSchemes` |
|
|
428
428
|
| ALPN | `h2, http/1.1` | `tls.alpn` |
|
|
429
|
-
| HTTP/2 `SETTINGS` ids **and order** | curl's: `MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH
|
|
429
|
+
| HTTP/2 `SETTINGS` ids **and order** | curl's: `MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH`. The **values** differ by default — see below | `http2Settings` |
|
|
430
430
|
| h2 preface, `WINDOW_UPDATE`, pseudo-header order | curl's, byte-for-byte | `http2ConnectionWindow`, `http2PseudoHeaderOrder` |
|
|
431
431
|
| HPACK representation | curl's (`:path` without indexing, the rest incremental) | `http2HpackIndexing` |
|
|
432
432
|
| `Accept-Encoding` | `gzip, deflate` — curl's | `decoders` appends |
|
|
433
433
|
|
|
434
|
+
**The default `SETTINGS` values are this package's, not curl's, and that is a deliberate trade.**
|
|
435
|
+
`profiles.curl` carries curl 8.21.0's real `INITIAL_WINDOW_SIZE` of **64 KiB**, captured and pinned
|
|
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 for throughput: a 64 KiB stream window means far more
|
|
438
|
+
`WINDOW_UPDATE` round trips on a large body, and the cost work in this README shows body transfer is
|
|
439
|
+
where the money is. **That throughput difference has not been measured**, so if you are moving large
|
|
440
|
+
bodies under `profiles.curl`, measure it before relying on it — or set `http2Settings` yourself.
|
|
441
|
+
|
|
442
|
+
Until 1.6.2 the profile carried curl **8.7.1's** window while presenting curl **8.21.0's**
|
|
443
|
+
ClientHello: one named client, two source versions, and a split identity that only a capture could
|
|
444
|
+
find, because each half was individually true of some curl.
|
|
445
|
+
|
|
434
446
|
Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
|
|
435
447
|
of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
|
|
436
448
|
§4.2.11 defines the binder transcript as the hello truncated just before the binders, which is a
|
package/README.zh-CN.md
CHANGED
|
@@ -334,11 +334,20 @@ curl(8.7.1 / nghttp2),照线上抓包原样复刻。这么做是实证需
|
|
|
334
334
|
| supported_groups | `x25519, secp256r1, secp384r1, secp521r1` | `tls.groups` |
|
|
335
335
|
| 签名算法 | SHA-256/384/512 上的 ECDSA 与 RSA-PSS/PKCS#1 | `tls.sigSchemes` |
|
|
336
336
|
| ALPN | `h2, http/1.1` | `tls.alpn` |
|
|
337
|
-
| HTTP/2 `SETTINGS` 的 id **与顺序** | curl 的:`MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH
|
|
337
|
+
| HTTP/2 `SETTINGS` 的 id **与顺序** | curl 的:`MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH`。**取值**默认不同——见下 | `http2Settings` |
|
|
338
338
|
| h2 前导、`WINDOW_UPDATE`、伪头顺序 | curl 的,逐字节一致 | `http2ConnectionWindow`、`http2PseudoHeaderOrder` |
|
|
339
339
|
| HPACK 表示 | curl 的(`:path` 不索引,其余 incremental) | `http2HpackIndexing` |
|
|
340
340
|
| `Accept-Encoding` | `gzip, deflate`——curl 的 | `decoders` 会追加 |
|
|
341
341
|
|
|
342
|
+
**默认的 `SETTINGS` 取值是本包自己的,不是 curl 的,这是一个有意的取舍。** `profiles.curl` 带的是
|
|
343
|
+
curl 8.21.0 实测的 `INITIAL_WINDOW_SIZE` = **64 KiB**,抓包固化在 `test/tls/_captured-h2.js`。而不加
|
|
344
|
+
profile 时连接的默认值是 **10 MiB**——那是 curl 8.7.1 发的值,本包为吞吐保留它:64 KiB 的流窗口意味着大
|
|
345
|
+
body 上要多得多的 `WINDOW_UPDATE` 往返,而本文档的成本部分已经说明,钱就花在 body 传输上。**这个吞吐差异
|
|
346
|
+
我没有实测过**,所以如果你在 `profiles.curl` 下搬大 body,先测再依赖,或者自己设 `http2Settings`。
|
|
347
|
+
|
|
348
|
+
1.6.2 之前,这个 profile 带着 curl **8.7.1** 的窗口,却呈现 curl **8.21.0** 的 ClientHello:同一个具名
|
|
349
|
+
客户端,两个来源版本——一个只有抓包才能发现的身份分裂,因为两半各自都真的属于某个 curl。
|
|
350
|
+
|
|
342
351
|
扩展顺序之所以要紧,是因为 JA3 和 JA4 哈希的正是**线上顺序**的扩展列表,那是指纹识别读到的主要内容。`pre_shared_key` 无论你怎么配都强制排最后:RFC 8446 §4.2.11 把 binder 的转录定义为"截到 binder 之前的那段 hello",只有后面不跟东西时这个范围才成立。
|
|
343
352
|
|
|
344
353
|
**默认值刻意与 curl 不同的地方**,以及为什么不能照抄:ClientHello 是一份**要约**,服务器可以接受其中任何一项。声明你做不到的事,等于拿指纹不一致换一次握手失败——后者更糟,而且是静默的。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.3",
|
|
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/http2/connection.js
CHANGED
|
@@ -201,7 +201,21 @@ export class Http2Connection {
|
|
|
201
201
|
|
|
202
202
|
// Our advertised settings. The defaults ARE the fingerprint (see constants.js); overrides
|
|
203
203
|
// exist for tests, and shifting them shifts what the server sees, so production leaves them.
|
|
204
|
-
|
|
204
|
+
// The window we ADVERTISE and the window we ACCOUNT FOR must be the same number, and until
|
|
205
|
+
// 1.6.3 they were two independent settings that silently disagreed.
|
|
206
|
+
//
|
|
207
|
+
// `opts.settings` is the SETTINGS flight, which is how a fingerprint profile states its
|
|
208
|
+
// identity; SETTINGS_INITIAL_WINDOW_SIZE (id 4) inside it is what the peer is told. This field
|
|
209
|
+
// is what `_replenish` measures against — it only emits a WINDOW_UPDATE once the consumer has
|
|
210
|
+
// drained half of it. Take them from different places and a profile advertising 64 KiB against
|
|
211
|
+
// a default accounting of 10 MiB sets a replenish threshold of 5 MiB that a 64 KiB window can
|
|
212
|
+
// never reach: the peer sends one window's worth, stops, and no WINDOW_UPDATE ever comes. Every
|
|
213
|
+
// response larger than the advertised window hangs until the idle deadline fires.
|
|
214
|
+
//
|
|
215
|
+
// That is not hypothetical — `profiles.curl` gained curl 8.21.0's real 65536 in 1.6.2 and this
|
|
216
|
+
// is what it did. One source of truth: if the flight names id 4, that IS the window.
|
|
217
|
+
const advertised = (opts.settings ?? []).find(([id]) => id === 0x4)?.[1];
|
|
218
|
+
this._ourInitialWindow = opts.initialWindowSize ?? advertised ?? CLIENT_INITIAL_WINDOW_SIZE;
|
|
205
219
|
this._ourConnWindow = opts.connectionWindow ?? CLIENT_CONNECTION_WINDOW;
|
|
206
220
|
this._ourMaxConcurrent = opts.maxConcurrentStreams ?? CLIENT_MAX_CONCURRENT_STREAMS;
|
|
207
221
|
this._ourHeaderTableSize = opts.maxHeaderTableSize ?? DEFAULT_HEADER_TABLE_SIZE;
|
package/src/profiles.js
CHANGED
|
@@ -10,8 +10,17 @@
|
|
|
10
10
|
// it declares what it REQUIRES — because a profile that quietly drops the half of itself this
|
|
11
11
|
// runtime cannot perform would recreate exactly the incoherence it exists to prevent.
|
|
12
12
|
//
|
|
13
|
-
// The values are captured, not recalled
|
|
14
|
-
//
|
|
13
|
+
// The values are captured, not recalled — and as of 1.6.2 the captures are COMMITTED, in
|
|
14
|
+
// test/tls/_captured-hellos.js, recorded by scripts/capture-clienthello.mjs from curl 8.21.0 /
|
|
15
|
+
// OpenSSL 3.6.3 and from Chromium. Until then the claim had no artifact behind it: the tests
|
|
16
|
+
// compared the builder against the constants below, which catches drift and cannot catch the
|
|
17
|
+
// constants being wrong about the client they name.
|
|
18
|
+
//
|
|
19
|
+
// Two things that check found, both now asserted rather than assumed:
|
|
20
|
+
// * curl's TLS backend is part of its identity. macOS system curl (8.7.1, LibreSSL) sends SEVEN
|
|
21
|
+
// extensions where the OpenSSL build sends twelve, so "curl" alone does not name a fingerprint.
|
|
22
|
+
// * Chromium leads signature_algorithms with ML-DSA-44/65/87 (0x0904/5/6), which this package
|
|
23
|
+
// cannot verify and therefore must not offer. The omission is correct and was undocumented.
|
|
15
24
|
|
|
16
25
|
import { ConfigError, codes } from './errors.js';
|
|
17
26
|
import { CURL_EXTENSION_ORDER, SHUFFLE_EXTENSIONS } from './tls/handshake-messages.js';
|
|
@@ -44,7 +53,14 @@ export const curl = Object.freeze({
|
|
|
44
53
|
headerOrder: CURL_HEADER_ORDER,
|
|
45
54
|
headers: Object.freeze([['User-Agent', 'curl/8.21.0']]),
|
|
46
55
|
// Captured: MAX_CONCURRENT_STREAMS, INITIAL_WINDOW_SIZE, ENABLE_PUSH, in that order.
|
|
47
|
-
|
|
56
|
+
//
|
|
57
|
+
// INITIAL_WINDOW_SIZE is 65536 and used to be 10485760, which was curl 8.7.1's value. curl changed
|
|
58
|
+
// it between 8.7.1 and 8.21.0, and this profile's TLS half is captured from 8.21.0 — so the
|
|
59
|
+
// package was presenting one curl's ClientHello above a different curl's SETTINGS. A split
|
|
60
|
+
// identity inside a single named client, and one only a capture could find: both halves were
|
|
61
|
+
// internally consistent and each was individually true of SOME curl. See test/tls/_captured-h2.js,
|
|
62
|
+
// which holds both recordings side by side for exactly this reason.
|
|
63
|
+
http2Settings: Object.freeze([[3, 100], [4, 65536], [2, 0]]),
|
|
48
64
|
http2PseudoHeaderOrder: Object.freeze([':method', ':scheme', ':authority', ':path']),
|
|
49
65
|
http2HpackIndexing: Object.freeze({ ':path': 'without' }),
|
|
50
66
|
requires: Object.freeze([]),
|
|
@@ -95,14 +111,18 @@ export const chrome = Object.freeze({
|
|
|
95
111
|
// both fingerprints are exactly what the browser normally sends.
|
|
96
112
|
http2Settings: Object.freeze([[1, 65536], [2, 0], [4, 6291456], [6, 262144]]),
|
|
97
113
|
http2ConnectionWindow: 15663105 + 65535,
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
//
|
|
105
|
-
//
|
|
114
|
+
// Captured in 1.6.2, and the answer was that there was nothing to change: Chromium emits `:path`
|
|
115
|
+
// WITHOUT indexing and everything else incrementally, byte-identical in representation to curl.
|
|
116
|
+
// 1.6.1 recorded this as an uncaptured gap and refused to guess at it; the guess would in fact
|
|
117
|
+
// have been right, which is not a reason to have guessed. It is declared explicitly now so the
|
|
118
|
+
// identity states its own value instead of inheriting one by accident.
|
|
119
|
+
http2HpackIndexing: Object.freeze({ ':path': 'without' }),
|
|
120
|
+
// STILL a real difference, and newly found by the same capture: Chromium sets the PRIORITY flag
|
|
121
|
+
// on its request HEADERS frame (flags 0x25) and carries the five deprecated priority bytes.
|
|
122
|
+
// This package never sets it (see frames.js headersFrame), so the frame layout differs from
|
|
123
|
+
// Chromium's even where every value above it matches. Not in `requires` because it is a framing
|
|
124
|
+
// detail this package deliberately does not emit — RFC 9113 s5.3.2 deprecates it — but it is a
|
|
125
|
+
// signal a frame-level fingerprinter reads, and it is written down here rather than discovered.
|
|
106
126
|
//
|
|
107
127
|
// The connection window immediately above was DEAD until 1.6.1 — declared here, copied by
|
|
108
128
|
// nothing, passed by nothing, and read under a different name — so every connection using this
|
package/types/profiles.d.ts
CHANGED
|
@@ -71,6 +71,9 @@ export const chrome: Readonly<{
|
|
|
71
71
|
}>;
|
|
72
72
|
http2Settings: readonly number[][];
|
|
73
73
|
http2ConnectionWindow: number;
|
|
74
|
+
http2HpackIndexing: Readonly<{
|
|
75
|
+
':path': "without";
|
|
76
|
+
}>;
|
|
74
77
|
http2PseudoHeaderOrder: readonly string[];
|
|
75
78
|
headerOrder: readonly string[];
|
|
76
79
|
headers: readonly string[][];
|