tunnelfetch 1.3.0 → 1.4.1
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 +93 -28
- package/README.zh-CN.md +54 -11
- package/package.json +5 -1
- package/src/client/decode.js +24 -3
- package/src/client.js +31 -4
- package/src/profile/chrome.js +45 -0
- package/src/profile/vendor/chacha20poly1305.js +65 -0
- package/src/profile/vendor/mlkem768.js +67 -0
- package/src/profiles.js +47 -13
- package/src/tls/aead.js +51 -16
- package/src/tls/connect.js +42 -7
- package/src/tls/constants.js +60 -6
- package/src/tls/handshake-messages.js +18 -5
- package/src/tls/handshake.js +1 -1
- package/src/tls/hybrid.js +166 -0
- package/src/tls/record.js +24 -3
- package/src/warmup-fixture.js +46 -45
- package/types/client/decode.d.ts +3 -1
- package/types/client.d.ts +34 -2
- package/types/profile/chrome.d.ts +16 -0
- package/types/profile/vendor/chacha20poly1305.d.ts +7 -0
- package/types/profile/vendor/mlkem768.d.ts +23 -0
- package/types/profiles.d.ts +6 -2
- package/types/tls/aead.d.ts +17 -1
- package/types/tls/connect.d.ts +30 -4
- package/types/tls/constants.d.ts +31 -3
- package/types/tls/handshake-messages.d.ts +8 -4
- package/types/tls/hybrid.d.ts +63 -0
- package/types/tls/record.d.ts +23 -0
package/README.md
CHANGED
|
@@ -278,6 +278,32 @@ it saves do not pay it back — see [What this cannot do](#what-this-cannot-do-a
|
|
|
278
278
|
are validated as HTTP tokens, a decoder that throws fails the body closed rather than truncating it,
|
|
279
279
|
and an unregistered coding is still refused.
|
|
280
280
|
|
|
281
|
+
### The Chrome identity, in one import
|
|
282
|
+
|
|
283
|
+
```js
|
|
284
|
+
import { Client } from 'tunnelfetch';
|
|
285
|
+
import { chrome } from 'tunnelfetch/profile/chrome';
|
|
286
|
+
|
|
287
|
+
const client = new Client({ profile: chrome, connect, proxy, decoders: { br, zstd } });
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
The subpath carries the two primitives this runtime has no native path for — ML-KEM-768 for the
|
|
291
|
+
`X25519MLKEM768` key exchange and ChaCha20-Poly1305 for the record layer, both compiled to
|
|
292
|
+
freestanding WASM and both with known-answer tests in this repository. **Importing it is the
|
|
293
|
+
opt-in:** a bundler pulls them in only for code on this path, so the default identity carries none
|
|
294
|
+
of it.
|
|
295
|
+
|
|
296
|
+
`br` and `zstd` stay yours. They are not cryptography and there is no single right implementation,
|
|
297
|
+
so the profile keeps refusing until you supply them — a Chrome that advertises `br` and cannot read
|
|
298
|
+
it is worse than one that says so.
|
|
299
|
+
|
|
300
|
+
Verified end to end, not merely constructed:
|
|
301
|
+
|
|
302
|
+
| Origin | | TLS | Group | HTTP |
|
|
303
|
+
|---|---|---|---|---|
|
|
304
|
+
| `blog.cloudflare.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
|
|
305
|
+
| `www.shopify.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
|
|
306
|
+
|
|
281
307
|
### HTTP/2 — access, not speed
|
|
282
308
|
|
|
283
309
|
The client offers `h2` and `http/1.1` in ALPN by default and speaks whichever the server selects.
|
|
@@ -344,16 +370,19 @@ trades a fingerprint mismatch for a broken handshake, which is worse and fails s
|
|
|
344
370
|
|
|
345
371
|
| curl sends | This package | Why |
|
|
346
372
|
|---|---|---|
|
|
347
|
-
| 30 cipher suites, incl. RSA key exchange and CBC | 6 AEAD suites |
|
|
348
|
-
| `X25519MLKEM768` group and a 1216-byte key share |
|
|
373
|
+
| 30 cipher suites, incl. ChaCha20, RSA key exchange and CBC | 6 AEAD suites, or 7 with ChaCha20 injected | RSA-kx and CBC are refused by design — a server selecting `TLS_RSA_WITH_AES_256_CBC_SHA` would get a dead connection. `TLS_CHACHA20_POLY1305_SHA256` is injectable via `ciphers: { chacha20 }`: WebCrypto has no ChaCha20 here, so an implementation is supplied rather than a `node:crypto` dependency taken |
|
|
374
|
+
| `X25519MLKEM768` group and a 1216-byte key share | offered only when injected | ML-KEM is not a WebCrypto primitive; supply it as `groups: { x25519mlkem768 }` (what `profiles.chrome` requires) and the group and its 1216-byte hybrid key share go on the wire |
|
|
349
375
|
| SHA-1 signature schemes | not offered | Refused deliberately |
|
|
350
376
|
| `encrypt_then_mac` | not sent | Applies only to CBC suites, which are not offered |
|
|
351
377
|
| `post_handshake_auth` | not sent | Invites a post-handshake `CertificateRequest`, which is not implemented |
|
|
352
378
|
| — | `status_request` | curl does not ask for a stapled OCSP response; this package must, because a staple is its only revocation signal |
|
|
353
379
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
380
|
+
ChaCha20-Poly1305 and X25519MLKEM768 are reachable by **injection**: an implementation of each is
|
|
381
|
+
supplied through `ciphers` / `groups`, which is precisely what `profiles.chrome` requires, and
|
|
382
|
+
neither is offered unless one is — a suite or group advertised but not performable is a dead
|
|
383
|
+
connection if a server takes it. RSA key exchange and CBC suites stay refused on purpose;
|
|
384
|
+
`tls.ciphers` and `tls.groups` will let you offer them anyway, and the handshake will then fail if a
|
|
385
|
+
server picks one, which is yours to own.
|
|
357
386
|
|
|
358
387
|
A test asserts this delta is exactly the list above, so gaining one of these capabilities without
|
|
359
388
|
updating the table fails the build.
|
|
@@ -381,7 +410,9 @@ res.tunnelfetch.httpVersion; // '2' if the server chose h2, '1.1' otherwise
|
|
|
381
410
|
| `connect` | — | Socket factory. On Workers, the `connect` export of `cloudflare:sockets`. Required for anything the platform's `fetch` cannot serve. |
|
|
382
411
|
| `proxy` | `null` | URL string or object. `http:`, `https:`, `socks5:`, `socks5h:`. |
|
|
383
412
|
| `trust` | `{mode:'system'}` | Certificate policy; see below. |
|
|
384
|
-
| `tls` | `{}` | Handshake options (`alpn`, `groups`, `ciphers`, `offerGroups`). |
|
|
413
|
+
| `tls` | `{}` | Handshake options (`alpn`, `groups`, `ciphers`, `offerGroups`). Here `groups`/`ciphers` are number lists — the suite and group ids to offer, in preference order. |
|
|
414
|
+
| `ciphers` | `{}` | Injected AEAD implementations by capability name: `{ chacha20 }` (a `seal`/`open` pair). WebCrypto has no ChaCha20 here, so `TLS_CHACHA20_POLY1305_SHA256` is offered only when this is supplied. Required by `profiles.chrome`. |
|
|
415
|
+
| `groups` | `{}` | Injected key-exchange implementations by capability name: `{ x25519mlkem768 }` (ML-KEM-768 `keygen`/`encapsulate`/`decapsulate`). ML-KEM is not a WebCrypto primitive, so the post-quantum hybrid group is offered only when this is supplied. Required by `profiles.chrome`. |
|
|
385
416
|
| `timeouts` | see below | `connectMs`, `handshakeMs`, `headersMs`, `idleMs`, `totalMs`. |
|
|
386
417
|
| `cookies` | `false` | Enable a per-Client cookie jar. |
|
|
387
418
|
| `maxRedirects` | `20` | |
|
|
@@ -493,9 +524,15 @@ Not implemented, and not planned:
|
|
|
493
524
|
since 2020. Note the distinction: a server that *also* supports 1.0/1.1 is fine, because we will
|
|
494
525
|
negotiate 1.2 or 1.3 with it. Only a server that supports *nothing else* is out of reach.
|
|
495
526
|
- **RSA key transport.** No forward secrecy.
|
|
496
|
-
- **ChaCha20-Poly1305
|
|
497
|
-
|
|
498
|
-
|
|
527
|
+
- **ChaCha20-Poly1305, unless injected.** WebCrypto has no ChaCha20 on this runtime, so it is not
|
|
528
|
+
built in — taking a `node:crypto` dependency would cost the package its "web platform only"
|
|
529
|
+
property. It is *injectable*, though: pass `ciphers: { chacha20 }` (a `seal`/`open` pair, e.g. a
|
|
530
|
+
WASM build) and `TLS_CHACHA20_POLY1305_SHA256` is offered — in curl's captured position, second
|
|
531
|
+
after AES-256-GCM — and used. Not built in because a server can only pick a suite we offered, TLS
|
|
532
|
+
1.3 mandates AES-128-GCM, and AES-GCM is universal in TLS 1.2; the reason to add it is matching a
|
|
533
|
+
browser that offers it (`profiles.chrome`), not compatibility. The post-quantum
|
|
534
|
+
**X25519MLKEM768** group is injectable the same way — `groups: { x25519mlkem768 }` — for the same
|
|
535
|
+
reason: ML-KEM is not a WebCrypto primitive here.
|
|
499
536
|
- **Client certificates (mTLS), 0-RTT, renegotiation.** A `HelloRequest` is refused rather than
|
|
500
537
|
honoured. 0-RTT is a decision rather than an omission: early data can be replayed, so offering
|
|
501
538
|
it would let an attacker who captured a POST replay it. (Session resumption itself *is*
|
|
@@ -565,25 +602,28 @@ Fetching a size-controlled origin through a proxy, warm, medians over seven-plus
|
|
|
565
602
|
isolate, gzip on the wire. The last column is the same numbers as a rate, which is the form worth
|
|
566
603
|
carrying around:
|
|
567
604
|
|
|
568
|
-
| | New connection | Each further request, same connection |
|
|
569
|
-
| --- | --- | --- |
|
|
570
|
-
| 1 KB body |
|
|
571
|
-
| 16 KB body |
|
|
572
|
-
| 64 KB body |
|
|
573
|
-
| 256 KB body | 7
|
|
574
|
-
| 1 MB body |
|
|
575
|
-
| 4 MB body |
|
|
576
|
-
| **
|
|
577
|
-
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
605
|
+
| | New connection (first request included) | Each further request, same connection |
|
|
606
|
+
| --- | --- | --- |
|
|
607
|
+
| 1 KB body | 6.4 ms | 1.6 ms |
|
|
608
|
+
| 16 KB body | 6.5 ms | 1.7 ms |
|
|
609
|
+
| 64 KB body | 6.7 ms | 1.9 ms |
|
|
610
|
+
| 256 KB body | 7.4 ms | 2.6 ms |
|
|
611
|
+
| 1 MB body | 10.4 ms | 5.6 ms |
|
|
612
|
+
| 4 MB body | 22.4 ms | 17.6 ms |
|
|
613
|
+
| **First request in a fresh isolate** | 46 ms | — |
|
|
614
|
+
| **…after `warmup({ iterations: 5 })`** | 16 ms | — |
|
|
615
|
+
|
|
616
|
+
Re-measured for 1.4.0 against a size-controlled origin through a proxy, ten rounds per size, HTTP/2
|
|
617
|
+
negotiated, gzip on the wire. The rows are not independent measurements — they are one model, fitted
|
|
618
|
+
to the sweep and then checked back against it:
|
|
619
|
+
|
|
620
|
+
> **≈ 6.4 ms to open a connection + 1.6 ms per further request + ~4 ms per MB of body**
|
|
621
|
+
|
|
622
|
+
Each sweep request fetches five pages — one on a fresh connection and four reusing it — so the two
|
|
623
|
+
terms were separated by varying the reuse count rather than assumed: two pages against ten gives
|
|
624
|
+
1.63 ms per further request, and the connection term falls out of the remainder. Fitted on 1 KB and
|
|
625
|
+
4 MB, the model predicts 32.9 ms for the 1 MB row against 35 measured, and 92.9 for the 4 MB row
|
|
626
|
+
against 94.
|
|
587
627
|
|
|
588
628
|
The ranges are real, not imprecision: absolute CPU on this platform varies by up to ~1.5× between
|
|
589
629
|
isolates and runs — the same sweep repeated lands on faster and slower machines — so the values are
|
|
@@ -609,6 +649,31 @@ excess above the warm floor without `warmup()`, 15 ms with it at five iterations
|
|
|
609
649
|
1.1 ms per request respectively, amortised over an isolate's early life. Warming costs 10 ms of
|
|
610
650
|
startup at one iteration and 22 ms at five, against a 1 s budget, and does not lower the warm floor.
|
|
611
651
|
|
|
652
|
+
### What the optional switches cost
|
|
653
|
+
|
|
654
|
+
Everything above is the default identity: curl's fingerprint, gzip and deflate, no post-quantum, no
|
|
655
|
+
GREASE. Each switch below is off unless asked for, and the table is what asking costs. Measured on
|
|
656
|
+
the edge the same way as the rest — differencing two work counts, minimum of samples.
|
|
657
|
+
|
|
658
|
+
| Switch | Cost | Paid |
|
|
659
|
+
|---|---|---|
|
|
660
|
+
| `grease: true` | not measurable | a handful of extra bytes in one hello |
|
|
661
|
+
| `tls.extensionOrder: 'shuffle'` | not measurable | shuffling ~11 items, once per handshake |
|
|
662
|
+
| `headerOrder` | not measurable — the ordered list is *faster* than the platform `Headers` (1.6 µs against 3.8 µs) | per request |
|
|
663
|
+
| `groups: { x25519mlkem768 }` | **+0.15 ms** with the bundled WASM, **+1.35 ms** with a pure-JS ML-KEM | per **connection**, not per request — amortised across every request that reuses it |
|
|
664
|
+
| `ciphers: { chacha20 }` | **+2.0 ms/MB**, and only if the server *selects* it | per byte. Servers with AES hardware generally prefer AES-GCM, so the usual cost is zero and the offer is what matters |
|
|
665
|
+
| `decoders: { br }` | **+4.4 ms/MB** | per byte, whenever an origin serves brotli. Harder compression is worse, not better: quality 11 is 16% smaller on the wire and 46% dearer to decode |
|
|
666
|
+
| `profile: chrome` | the sum of the three above | |
|
|
667
|
+
|
|
668
|
+
Two defaults moved in 1.4.0 and neither is visible in the table above them: matching curl's cipher
|
|
669
|
+
order means AES-256-GCM is negotiated where AES-128-GCM used to be, measured at **+4%** per MB
|
|
670
|
+
(1.50 against 1.45 ms/MB — hardware AES makes the extra rounds cheap), and the ordered header list
|
|
671
|
+
replaced the platform `Headers`, which is slightly *cheaper*. Both are inside the ±1.5× spread the
|
|
672
|
+
figures above already carry.
|
|
673
|
+
|
|
674
|
+
Bundle: importing `tunnelfetch/profile/chrome` adds **~22 KB gzipped** for the two WASM primitives.
|
|
675
|
+
Nothing else imports them, so a caller on the default identity carries none of it.
|
|
676
|
+
|
|
612
677
|
### What that costs in dollars
|
|
613
678
|
|
|
614
679
|
Workers Standard bills $5/month including 10 million requests and 30 million CPU milliseconds, then
|
package/README.zh-CN.md
CHANGED
|
@@ -219,6 +219,26 @@ const client = new Client({ connect, proxy, decoders: { br: brotli } });
|
|
|
219
219
|
|
|
220
220
|
brotli 本身落在原生 inflate 的 1.9 倍。这个差价就是这个编码的成本,而它省下的线上字节买不回来——见[这个包做不到什么](#这个包做不到什么为什么)。解码器名字会按 HTTP token 校验;解码器抛错时响应体 fail closed,而不是被截断;未注册的编码依然会被拒绝。
|
|
221
221
|
|
|
222
|
+
### 一行 import 得到 Chrome 身份
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
import { Client } from 'tunnelfetch';
|
|
226
|
+
import { chrome } from 'tunnelfetch/profile/chrome';
|
|
227
|
+
|
|
228
|
+
const client = new Client({ profile: chrome, connect, proxy, decoders: { br, zstd } });
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
这个子路径带着运行时没有原生实现的两个原语——`X25519MLKEM768` 密钥交换要的 ML-KEM-768,以及记录层要的 ChaCha20-Poly1305,都编译成无依赖的 WASM,并且都在本仓库里有已知答案测试。**导入即 opt-in**:打包器只为走这条路径的代码拉入它们,默认身份一个字节都不背。
|
|
232
|
+
|
|
233
|
+
`br` 和 `zstd` 仍然要你自己带。它们不是密码学、没有唯一正解,所以 profile 会一直拒绝到你提供为止——**一个声称支持 br 却读不了的 Chrome,比一个老实说不支持的更糟**。
|
|
234
|
+
|
|
235
|
+
不是「能构造出来」,是端到端验证过:
|
|
236
|
+
|
|
237
|
+
| 源站 | | TLS | 群 | HTTP |
|
|
238
|
+
|---|---|---|---|---|
|
|
239
|
+
| `blog.cloudflare.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
|
|
240
|
+
| `www.shopify.com` | 200 | 1.3 | `0x11ec` X25519MLKEM768 | h2 |
|
|
241
|
+
|
|
222
242
|
### HTTP/2 — 要的是访问,不是速度
|
|
223
243
|
|
|
224
244
|
客户端默认在 ALPN 中同时报出 `h2` 与 `http/1.1`,服务器选中哪个就说哪个。没有单独的 API:
|
|
@@ -410,17 +430,22 @@ CertificateError [CERT_PIN_MISMATCH]: no certificate in the chain matches any co
|
|
|
410
430
|
通过代理抓取一个尺寸可控的源站,热态,同一 isolate 上 7 轮以上取中位数,传输走 gzip。最后一列是同样的数字
|
|
411
431
|
换算成速率,那是更值得随身记住的形式:
|
|
412
432
|
|
|
413
|
-
| | 新连接 |
|
|
414
|
-
| --- | --- | --- |
|
|
415
|
-
| 1 KB body |
|
|
416
|
-
| 16 KB body |
|
|
417
|
-
| 64 KB body |
|
|
418
|
-
| 256 KB body | 7
|
|
419
|
-
| 1 MB body |
|
|
420
|
-
| 4 MB body |
|
|
421
|
-
|
|
|
422
|
-
|
|
|
423
|
-
|
|
433
|
+
| | 新连接(含首个请求) | 同连接每次后续请求 |
|
|
434
|
+
| --- | --- | --- |
|
|
435
|
+
| 1 KB body | 6.4 ms | 1.6 ms |
|
|
436
|
+
| 16 KB body | 6.5 ms | 1.7 ms |
|
|
437
|
+
| 64 KB body | 6.7 ms | 1.9 ms |
|
|
438
|
+
| 256 KB body | 7.4 ms | 2.6 ms |
|
|
439
|
+
| 1 MB body | 10.4 ms | 5.6 ms |
|
|
440
|
+
| 4 MB body | 22.4 ms | 17.6 ms |
|
|
441
|
+
| **全新 isolate 的第一个请求** | 46 ms | — |
|
|
442
|
+
| **…执行 `warmup({ iterations: 5 })` 之后** | 16 ms | — |
|
|
443
|
+
|
|
444
|
+
为 1.4.0 重新测量:经代理打一个尺寸可控的源站,每个尺寸十轮,协商 HTTP/2,线上走 gzip。这些行**不是各自独立的测量**,而是一个模型拟合出来再回代验证的:
|
|
445
|
+
|
|
446
|
+
> **≈ 开一条连接 6.4 ms + 每次后续请求 1.6 ms + 每 MB body 约 4 ms**
|
|
447
|
+
|
|
448
|
+
每次扫描请求会抓五个页面——一个走新连接、四个复用它——所以两项是靠**改变复用次数**分离出来的,不是假设的:两页对十页得出每次后续请求 1.63 ms,连接项由余数得到。用 1 KB 和 4 MB 拟合,模型预测 1 MB 那行 32.9 ms(实测 35)、4 MB 那行 92.9 ms(实测 94)。
|
|
424
449
|
|
|
425
450
|
有一个模型能把每一个尺寸行都拟合到它的散布之内:
|
|
426
451
|
|
|
@@ -517,6 +542,24 @@ inflate 本身(约 2 ms/MB)加上把 body 物化成 JS 字符串(约 1.7 m
|
|
|
517
542
|
导入这个包是免费的。121 个内置锚是以主题 DN 哈希为索引的 base64 字符串,只有链实际落到的那一个会被解码,所以
|
|
518
543
|
380 KB 打包(gzip 后 133 KB)的启动时间保持在约 2 ms,而一个导入了但没使用本包的请求是 0 ms。
|
|
519
544
|
|
|
545
|
+
### 可选开关各自的成本
|
|
546
|
+
|
|
547
|
+
上面所有数字都是默认身份:curl 指纹、gzip 与 deflate、无后量子、无 GREASE。下面每个开关默认都关着,这张表是「打开它要付什么」。测法与其余部分一致——差分两个工作量、取样本最小值。
|
|
548
|
+
|
|
549
|
+
| 开关 | 成本 | 何时付 |
|
|
550
|
+
|---|---|---|
|
|
551
|
+
| `grease: true` | 测不出来 | 一次 hello 里多几个字节 |
|
|
552
|
+
| `tls.extensionOrder: 'shuffle'` | 测不出来 | 每次握手洗牌约 11 个元素 |
|
|
553
|
+
| `headerOrder` | 测不出来——有序列表比平台的 `Headers` **更快**(1.6 µs 对 3.8 µs) | 每请求 |
|
|
554
|
+
| `groups: { x25519mlkem768 }` | 用内置 WASM **+0.15 ms**,用纯 JS 的 ML-KEM **+1.35 ms** | **每连接**,不是每请求——复用该连接的所有请求共同摊薄 |
|
|
555
|
+
| `ciphers: { chacha20 }` | **+2.0 ms/MB**,而且只有服务器**选中**它时才付 | 每字节。有 AES 硬件加速的服务器通常偏好 AES-GCM,所以实际成本往往是零,真正起作用的是"出现在 offer 里" |
|
|
556
|
+
| `decoders: { br }` | **+4.4 ms/MB** | 每字节,只要源站发 brotli。压得越狠越亏:quality 11 线上小 16%,解码贵 46% |
|
|
557
|
+
| `profile: chrome` | 上面三项之和 | |
|
|
558
|
+
|
|
559
|
+
1.4.0 里有两个默认值变了,而它们都没有出现在上面那张表里:套件顺序对齐 curl 之后,原本协商 AES-128-GCM 的地方现在协商 AES-256-GCM,实测**贵 4%**(1.50 对 1.45 ms/MB——硬件 AES 让多出来的轮次很便宜);有序 header 列表取代了平台 `Headers`,反而**略微更便宜**。两者都落在上面数字本来就带的 ±1.5× 波动之内。
|
|
560
|
+
|
|
561
|
+
体积:`import 'tunnelfetch/profile/chrome'` 会为两个 WASM 原语增加约 **22 KB(gzip)**。没有其它地方引用它们,所以走默认身份的使用者一个字节都不背。
|
|
562
|
+
|
|
520
563
|
### 计划限制
|
|
521
564
|
|
|
522
565
|
付费计划上 30 秒的默认 CPU 上限不是约束点——那大约是一次调用里 3000 条连接或 1 GB 的 body,而"同时等待响应头
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
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",
|
|
@@ -51,6 +51,10 @@
|
|
|
51
51
|
"./roots": {
|
|
52
52
|
"types": "./types/trust/roots.d.ts",
|
|
53
53
|
"default": "./src/trust/roots.js"
|
|
54
|
+
},
|
|
55
|
+
"./profile/chrome": {
|
|
56
|
+
"types": "./types/profile/chrome.d.ts",
|
|
57
|
+
"default": "./src/profile/chrome.js"
|
|
54
58
|
}
|
|
55
59
|
},
|
|
56
60
|
"types": "./types/index.d.ts",
|
package/src/client/decode.js
CHANGED
|
@@ -106,7 +106,12 @@ const DECOMPRESS_READ_BYTES = 65536;
|
|
|
106
106
|
* consumes many input chunks before producing output, so tying input progress to output
|
|
107
107
|
* pulls would deadlock.
|
|
108
108
|
*/
|
|
109
|
-
function decompressionStage(source, coding) {
|
|
109
|
+
function decompressionStage(source, coding, maxBytes = Infinity) {
|
|
110
|
+
// Bytes this stage has produced. `maxBodyBytes` bounded only the COMPRESSED wire body, so a
|
|
111
|
+
// caller asking for at most 1 MB received 20 MB from a 20 KB gzip bomb — the cap was applied to
|
|
112
|
+
// the wrong side of the decompressor. gzip reaches roughly 1000:1, so the gap was not bounded in
|
|
113
|
+
// any useful sense. Counted here, per stage, so a chain like `br, gzip` cannot exceed it either.
|
|
114
|
+
let produced = 0;
|
|
110
115
|
const srcReader = source.getReader();
|
|
111
116
|
/** Rejections here surface through the output stream; pre-observed like chunked.js does. */
|
|
112
117
|
let pumpDone = null;
|
|
@@ -207,6 +212,18 @@ function decompressionStage(source, coding) {
|
|
|
207
212
|
return;
|
|
208
213
|
}
|
|
209
214
|
if (value.byteLength === 0) continue; // legal, carries nothing; keep reading
|
|
215
|
+
produced += value.byteLength;
|
|
216
|
+
if (produced > maxBytes) {
|
|
217
|
+
// Refused BEFORE the over-long chunk is handed on, so the caller never holds more than
|
|
218
|
+
// it asked for. Fail closed: a truncated body delivered as if complete would be worse.
|
|
219
|
+
throw new HttpError(
|
|
220
|
+
codes.LIMIT_BODY,
|
|
221
|
+
`decoded body exceeded maxBodyBytes: ${produced} bytes of "${coding}" output past a ` +
|
|
222
|
+
`${maxBytes} byte cap. The compressed body was within the cap; the decompressed ` +
|
|
223
|
+
'one is what a gzip bomb inflates.',
|
|
224
|
+
{ coding, produced, maxBytes },
|
|
225
|
+
);
|
|
226
|
+
}
|
|
210
227
|
c.enqueue(value);
|
|
211
228
|
return;
|
|
212
229
|
}
|
|
@@ -263,9 +280,11 @@ function firstBytes(chunks, i) {
|
|
|
263
280
|
* comma-separated list names codings in the order the SERVER applied them, so decoding
|
|
264
281
|
* applies them in reverse.
|
|
265
282
|
* @param {Record<string, BodyDecoder> | null} [decoders] caller-supplied codings
|
|
283
|
+
* @param {number} [maxBytes] cap on DECODED output, per stage. `maxBodyBytes` alone bounded the
|
|
284
|
+
* compressed body, which a gzip bomb walks straight past.
|
|
266
285
|
* @returns {ReadableStream<Uint8Array>} decoded bytes
|
|
267
286
|
*/
|
|
268
|
-
export function decodeBody(stream, contentEncoding, decoders = null) {
|
|
287
|
+
export function decodeBody(stream, contentEncoding, decoders = null, maxBytes = Infinity) {
|
|
269
288
|
/** Look a coding up among the caller's decoders, case-insensitively as the header is. */
|
|
270
289
|
const custom = (coding) => {
|
|
271
290
|
if (!decoders) return null;
|
|
@@ -311,6 +330,8 @@ export function decodeBody(stream, contentEncoding, decoders = null) {
|
|
|
311
330
|
// point can be reached with the other's assumption.
|
|
312
331
|
const fn = BUILT_IN.has(coding) ? null : custom(coding);
|
|
313
332
|
if (fn) {
|
|
333
|
+
// A caller-supplied decoder is not wrapped by the cap: it is their code producing their
|
|
334
|
+
// bytes, and silently truncating its output would be worse than leaving the bound to them.
|
|
314
335
|
const staged = fn(out);
|
|
315
336
|
// A decoder that returns something unreadable would surface far downstream as a confusing
|
|
316
337
|
// stream error; name it here, where the caller can see which coding misbehaved.
|
|
@@ -324,7 +345,7 @@ export function decodeBody(stream, contentEncoding, decoders = null) {
|
|
|
324
345
|
out = staged;
|
|
325
346
|
continue;
|
|
326
347
|
}
|
|
327
|
-
out = decompressionStage(out, coding === 'x-gzip' ? 'gzip' : coding);
|
|
348
|
+
out = decompressionStage(out, coding === 'x-gzip' ? 'gzip' : coding, maxBytes);
|
|
328
349
|
}
|
|
329
350
|
return out;
|
|
330
351
|
}
|
package/src/client.js
CHANGED
|
@@ -72,7 +72,9 @@ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
|
72
72
|
* @property {import('./client/cookies.js').CookieJar} [jar] supply a jar directly, e.g. to share
|
|
73
73
|
* one across Clients or to persist it.
|
|
74
74
|
* @property {number} [maxRedirects] default 20.
|
|
75
|
-
* @property {number} [maxBodyBytes]
|
|
75
|
+
* @property {number} [maxBodyBytes] the most body this client will produce. Checked against
|
|
76
|
+
* Content-Length before a byte is read, enforced on the raw stream, and enforced again on the
|
|
77
|
+
* DECODED output — a compressed body within the cap can decompress far past it.
|
|
76
78
|
* @property {boolean} [decompress] gzip/deflate. Default true.
|
|
77
79
|
* @property {Record<string, import('./client/decode.js').BodyDecoder>} [decoders] extra
|
|
78
80
|
* content-codings this client can read, e.g. `{ br: (s) => ... }`. Registering one is what
|
|
@@ -83,6 +85,15 @@ const NULL_BODY_STATUS = new Set([101, 204, 205, 304]);
|
|
|
83
85
|
* yours and visible. Measured on the edge: WASM brotli decodes at about 2x native gzip, and
|
|
84
86
|
* the wire bytes it saves do not pay that back — see the README. The reason to turn it on is
|
|
85
87
|
* matching a browser's Accept-Encoding, not saving CPU.
|
|
88
|
+
* @property {{ chacha20?: import('./tls/aead.js').AeadOptions['impl'] }} [ciphers] injected AEAD
|
|
89
|
+
* implementations, by capability name. `chacha20` (seal/open, RFC 8439) is what lets
|
|
90
|
+
* TLS_CHACHA20_POLY1305_SHA256 be offered and performed — WebCrypto has no ChaCha20 on this
|
|
91
|
+
* runtime, and a suite offered but not performable is a dead connection if a server selects it,
|
|
92
|
+
* so without this the suite stays out of the ClientHello. Required by `profiles.chrome`.
|
|
93
|
+
* @property {{ x25519mlkem768?: import('./tls/hybrid.js').MlKem768 }} [groups] injected key-exchange
|
|
94
|
+
* implementations, by capability name. `x25519mlkem768` (ML-KEM-768 keygen/encapsulate/
|
|
95
|
+
* decapsulate) is what lets the post-quantum hybrid group be offered and performed; without it
|
|
96
|
+
* the group stays out of the ClientHello. Required by `profiles.chrome`.
|
|
86
97
|
* @property {boolean} [keepAlive] default true.
|
|
87
98
|
* @property {import('./profiles.js').FingerprintProfile} [profile] one coherent network identity
|
|
88
99
|
* instead of a dozen knobs that can disagree — TLS, HTTP/2, header order and default headers
|
|
@@ -403,7 +414,7 @@ async function openFreshAndSend(client, current, { hop, key, proxy, trust, tls }
|
|
|
403
414
|
tls,
|
|
404
415
|
alpn,
|
|
405
416
|
resumption: resumptionFor(client, key),
|
|
406
|
-
deps: o
|
|
417
|
+
deps: tlsDeps(o),
|
|
407
418
|
deadlines,
|
|
408
419
|
limits: o.limits ?? {},
|
|
409
420
|
now: o.now,
|
|
@@ -597,6 +608,22 @@ function serverNeverSawIt(err) {
|
|
|
597
608
|
return err instanceof TunnelFetchError && err.code === codes.TLS_TRUNCATED && err.detail?.got === 0;
|
|
598
609
|
}
|
|
599
610
|
|
|
611
|
+
/**
|
|
612
|
+
* The TLS deps for a connection: the caller's injectable randomness/keygen, plus the injected
|
|
613
|
+
* crypto implementations (`ciphers` -> `aead`, `groups` -> `kem`) the TLS layer needs to offer and
|
|
614
|
+
* perform the capability-gated ChaCha20 suite and X25519MLKEM768 group. Folded in here rather than
|
|
615
|
+
* carried in `tls`, so they neither enter the pool key nor disable native-fetch delegation — they
|
|
616
|
+
* are injected primitives, not fingerprint configuration.
|
|
617
|
+
* @param {Readonly<import('./client.js').ClientOptions>} o
|
|
618
|
+
* @returns {import('./tls/connect.js').TlsDeps}
|
|
619
|
+
*/
|
|
620
|
+
function tlsDeps(o) {
|
|
621
|
+
const deps = { ...(o.deps ?? {}) };
|
|
622
|
+
if (o.ciphers) deps.aead = o.ciphers;
|
|
623
|
+
if (o.groups) deps.kem = o.groups;
|
|
624
|
+
return deps;
|
|
625
|
+
}
|
|
626
|
+
|
|
600
627
|
async function sendAndReceive(client, conn, current, { key, deadlines, reused }) {
|
|
601
628
|
const o = client.options;
|
|
602
629
|
const target = targetFromUrl(current.url);
|
|
@@ -638,7 +665,7 @@ async function sendAndReceive(client, conn, current, { key, deadlines, reused })
|
|
|
638
665
|
trust: o.trust ?? { mode: 'system' },
|
|
639
666
|
tls: o.tls ?? {},
|
|
640
667
|
resumption: resumptionFor(client, key),
|
|
641
|
-
deps: o
|
|
668
|
+
deps: tlsDeps(o),
|
|
642
669
|
deadlines,
|
|
643
670
|
limits: o.limits ?? {},
|
|
644
671
|
now: o.now,
|
|
@@ -800,7 +827,7 @@ function decodeResponseBody(body, headers, options) {
|
|
|
800
827
|
if (options.decompress === false) return body;
|
|
801
828
|
const encoding = headers.get('content-encoding');
|
|
802
829
|
if (!encoding) return body;
|
|
803
|
-
return decodeBody(body, encoding, options.decoders ?? null);
|
|
830
|
+
return decodeBody(body, encoding, options.decoders ?? null, options.maxBodyBytes ?? Infinity);
|
|
804
831
|
}
|
|
805
832
|
|
|
806
833
|
function buildResponse(headInfo, body, framing, conn) {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The Chrome identity, ready to use. Importing this module IS the opt-in.
|
|
2
|
+
//
|
|
3
|
+
// `profiles.chrome` in the main entry is a declaration: it names what the identity needs and
|
|
4
|
+
// REFUSES to be presented without it, so nobody accidentally ships a Chromium ClientHello over a
|
|
5
|
+
// curl handshake. That refusal is correct but it leaves the caller to find and wire two WASM
|
|
6
|
+
// primitives, which is friction for the common case of "I want Chrome".
|
|
7
|
+
//
|
|
8
|
+
// This module closes that gap without moving the cost onto anyone else. The main entry stays free
|
|
9
|
+
// of both blobs — a bundler only pulls them in for code that imports THIS path, so a caller using
|
|
10
|
+
// the curl default pays nothing. That is the whole reason it is a separate entry point rather than
|
|
11
|
+
// a flag.
|
|
12
|
+
//
|
|
13
|
+
// import { Client } from 'tunnelfetch';
|
|
14
|
+
// import { chrome } from 'tunnelfetch/profile/chrome';
|
|
15
|
+
// new Client({ profile: chrome, connect, proxy });
|
|
16
|
+
//
|
|
17
|
+
// Still not supplied here: `br` and `zstd` decoders. Those are not cryptography and there is no
|
|
18
|
+
// single right implementation — bring your own through `decoders` (see the README). The profile
|
|
19
|
+
// will keep refusing until you do, which is the point.
|
|
20
|
+
|
|
21
|
+
import { chrome as declaration } from '../profiles.js';
|
|
22
|
+
import { chacha20poly1305 } from './vendor/chacha20poly1305.js';
|
|
23
|
+
import { mlkem768 } from './vendor/mlkem768.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `profiles.chrome` with the two capabilities this package cannot perform natively already wired
|
|
27
|
+
* in. Everything else about it — the cipher list, the groups, the shuffled extension order, GREASE,
|
|
28
|
+
* the HTTP/2 SETTINGS and pseudo-header order, the request header order — comes from the
|
|
29
|
+
* declaration unchanged, and all of it was captured off the wire from Chrome 150.
|
|
30
|
+
*
|
|
31
|
+
* @type {import('../profiles.js').FingerprintProfile & {
|
|
32
|
+
* ciphers: Record<string, unknown>, groups: Record<string, unknown> }}
|
|
33
|
+
*/
|
|
34
|
+
export const chrome = Object.freeze({
|
|
35
|
+
...declaration,
|
|
36
|
+
name: `${declaration.name} (with bundled ML-KEM and ChaCha20)`,
|
|
37
|
+
// These satisfy two of the four entries in `requires`. `decoder:br` and `decoder:zstd` remain the
|
|
38
|
+
// caller's, so constructing a Client with this profile still fails until they are supplied —
|
|
39
|
+
// deliberately, because a Chrome that advertises `br` and cannot read it is worse than one that
|
|
40
|
+
// says so up front.
|
|
41
|
+
ciphers: Object.freeze({ chacha20: chacha20poly1305 }),
|
|
42
|
+
groups: Object.freeze({ x25519mlkem768: mlkem768 }),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export { chacha20poly1305, mlkem768 };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// ChaCha20-Poly1305 (RFC 8439 IETF AEAD) — libsodium reference C (DJB chacha-merged +
|
|
2
|
+
// poly1305-donna32 + the _ietf_encrypt/decrypt_detached construction) compiled to
|
|
3
|
+
// freestanding wasm32 with wasi-sdk 25 / LLVM 19. Zero wasm imports, no WASI, no
|
|
4
|
+
// allocator. Sources and build: wasmcrypto/c/chacha20poly1305.c, wasmcrypto/build.sh.
|
|
5
|
+
// AUTO-GENERATED by wasmcrypto/gen-js.mjs — edit that, not this.
|
|
6
|
+
// wasm sha256[0..12): ec15f83a678a raw 8839 bytes
|
|
7
|
+
|
|
8
|
+
const B64 = "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OgC+ASACQQA6AL8BIABBvxBqLQAAIQUgAEG+EGotAAAhAyAAQb0Qai0AACEGIABBvBBqLQAAIQcgAEG7EGotAAAhCCAAQboQai0AACEJIABBuRBqLQAAIQogAEG4EGotAAAhCyAAQbcQai0AACEMIABBthBqLQAAIQ0gAEG1EGotAAAhDiAAQbQQai0AACEPIABBsxBqLQAAIRAgAEGyEGotAAAhESAAQbEQai0AACAAQbAQai0AACEAIAItAA8hEyACLQAOIRQgAi0ADSEVIAItAAwhFiACLQALIRcgAi0ACiEYIAItAAkhGSACLQAIIRogAi0AByEbIAItAAYhHCACLQAFIR0gAi0ABCEeIAItAAMhHyACLQACISAgAi0AASACLQAAISIgAkEAOgAAIAJBADoAASACQQA6AAIgAkEAOgADIAJBADoABCACQQA6AAUgAkEAOgAGIAJBADoAByACQQA6AAggAkEAOgAJIAJBADoACiACQQA6AAsgAkEAOgAMIAJBADoADSACQQA6AA4gAkEAOgAPQX8hBHMgACAic3IgESAgc3IgECAfc3IgDyAec3IgDiAdc3IgDSAcc3IgDCAbc3IgCyAac3IgCiAZc3IgCSAYc3IgCCAXc3IgByAWc3IgBiAVc3IgAyAUc3IgBSATc3JBAWtBgAJxRQRAQcAQIAEQAAwBC0HAEEHAECAjQQEQCiABIQQLIAJBwAFqJAAgBAvOCQE3fyMAQUBqIQkgA0IAUgRAIAAoAjwhHSAAKAI4IR4gACgCNCESIAAoAjAhEyAAKAIsIR8gACgCKCEgIAAoAiQhISAAKAIgISIgACgCHCEjIAAoAhghJCAAKAIUISUgACgCECEmIAAoAgwhJyAAKAIIISggACgCBCEpIAAoAgAhKiAJQThqISwgCUEwaiEtIAlBKGohLiAJQSBqIS8gCUEYaiEwIAlBEGohMQNAAkAgA0I/VgRAIAIhBQwBCyAsQgA3AwAgLUIANwMAIC5CADcDACAvQgA3AwAgMEIANwMAIDFCADcDACAJQgA3AwggCUIANwMAQQAhBANAIAQgCWogASAEai0AADoAACADIARBAWoiBK1WDQALIAkiBSEBIAIhKwtBbCEWICohCCApIQogKCEOICchFCAmIQQgJSECICQhBiAjIQcgIiELICEhDyAgIQwgHSEQIB4h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";
|
|
9
|
+
|
|
10
|
+
function fromBase64(s) {
|
|
11
|
+
const bin = atob(s);
|
|
12
|
+
const u = new Uint8Array(bin.length);
|
|
13
|
+
for (let i = 0; i < u.length; i++) u[i] = bin.charCodeAt(i);
|
|
14
|
+
return u;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Module scope: on workerd this runs at isolate startup, which is not billed as request CPU.
|
|
18
|
+
// SYNCHRONOUS compile+instantiate, deliberately: the async forms (WebAssembly.compile /
|
|
19
|
+
// .instantiate) return promises that do not settle during workerd's startup evaluation, and
|
|
20
|
+
// the deploy is rejected with error 10021 "Top-level await in module is unsettled" — measured,
|
|
21
|
+
// not assumed. The module has zero imports; it cannot touch anything outside its own memory.
|
|
22
|
+
const _instance = new WebAssembly.Instance(new WebAssembly.Module(fromBase64(B64)), {});
|
|
23
|
+
const x = _instance.exports;
|
|
24
|
+
|
|
25
|
+
export const buildId = "ec15f83a678a";
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
const MSG = x.cc_msg(), AAD = x.cc_aad(), KEY = x.cc_key(), NONCE = x.cc_nonce();
|
|
29
|
+
const MSG_CAP = x.cc_msg_cap(), AAD_CAP = x.cc_aad_cap();
|
|
30
|
+
let mem = new Uint8Array(x.memory.buffer);
|
|
31
|
+
const view = () => (mem.buffer === x.memory.buffer ? mem : (mem = new Uint8Array(x.memory.buffer)));
|
|
32
|
+
const EMPTY = new Uint8Array(0);
|
|
33
|
+
|
|
34
|
+
function load(key, nonce, data, aad) {
|
|
35
|
+
if (key.length !== 32) throw new RangeError('key must be 32 bytes');
|
|
36
|
+
if (nonce.length !== 12) throw new RangeError('nonce must be 12 bytes');
|
|
37
|
+
if (aad.length > AAD_CAP) throw new RangeError('aad too long');
|
|
38
|
+
const m = view();
|
|
39
|
+
m.set(key, KEY);
|
|
40
|
+
m.set(nonce, NONCE);
|
|
41
|
+
m.set(aad, AAD);
|
|
42
|
+
m.set(data, MSG);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const chacha20poly1305 = {
|
|
46
|
+
/** seal(key32, nonce12, plaintext, aad?) -> ciphertext||tag (RFC 8439 AEAD) */
|
|
47
|
+
seal(key, nonce, plaintext, aad = EMPTY) {
|
|
48
|
+
if (plaintext.length > MSG_CAP) throw new RangeError('plaintext too long');
|
|
49
|
+
load(key, nonce, plaintext, aad);
|
|
50
|
+
const n = x.cc_seal(plaintext.length, aad.length);
|
|
51
|
+
if (n < 0) throw new RangeError('seal rejected input');
|
|
52
|
+
return view().slice(MSG, MSG + n);
|
|
53
|
+
},
|
|
54
|
+
/** open(key32, nonce12, ciphertextAndTag, aad?) -> plaintext; throws on auth failure */
|
|
55
|
+
open(key, nonce, ciphertext, aad = EMPTY) {
|
|
56
|
+
if (ciphertext.length < 16 || ciphertext.length > MSG_CAP + 16) {
|
|
57
|
+
throw new Error('chacha20poly1305: invalid ciphertext');
|
|
58
|
+
}
|
|
59
|
+
load(key, nonce, ciphertext, aad);
|
|
60
|
+
const n = x.cc_open(ciphertext.length, aad.length);
|
|
61
|
+
if (n < 0) throw new Error('chacha20poly1305: authentication failed');
|
|
62
|
+
return view().slice(MSG, MSG + n);
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
|