tunnelfetch 1.8.4 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -0
- package/package.json +1 -1
- package/src/proxy/http-connect.js +9 -3
- package/src/proxy/index.js +11 -1
- package/src/tls/connect.js +67 -1
- package/src/tls/handshake-messages.js +13 -4
- package/src/tls/handshake.js +9 -1
- package/src/transport.js +14 -0
- package/types/proxy/index.d.ts +7 -0
- package/types/tls/connect.d.ts +57 -2
- package/types/tls/handshake-messages.d.ts +7 -1
package/README.md
CHANGED
|
@@ -453,6 +453,63 @@ Until 1.6.2 the profile carried curl **8.7.1's** window while presenting curl **
|
|
|
453
453
|
ClientHello: one named client, two source versions, and a split identity that only a capture could
|
|
454
454
|
find, because each half was individually true of some curl.
|
|
455
455
|
|
|
456
|
+
**`extensionOrder` arranges extensions; it cannot add them.** The builder filters to the extensions
|
|
457
|
+
it actually generated and sorts those, so an identity needing one this package does not build is not
|
|
458
|
+
reachable by ordering. Chromium sends five that it does not:
|
|
459
|
+
|
|
460
|
+
| | |
|
|
461
|
+
| --- | --- |
|
|
462
|
+
| `signed_certificate_timestamp` | 18 |
|
|
463
|
+
| `compress_certificate` | 27 |
|
|
464
|
+
| `session_ticket` | 35 |
|
|
465
|
+
| `application_settings` | 17613 |
|
|
466
|
+
| `encrypted_client_hello` | 65037 |
|
|
467
|
+
|
|
468
|
+
So `profiles.chrome` presents a Chromium cipher list, group list and GREASE placement over an
|
|
469
|
+
extension **set** that is curl's. A test reads that gap straight out of the committed Chromium
|
|
470
|
+
capture and fails if it changes, so it cannot drift quietly — but it is a real difference and a
|
|
471
|
+
JA3/JA4 hash sees it.
|
|
472
|
+
|
|
473
|
+
`tls.omitExtensions` is the subtractive counterpart, and `status_request` (5) is what it exists for:
|
|
474
|
+
that is the one extension this package sends which curl does not, so an identity matching a sample
|
|
475
|
+
without it had no way to drop it. Dropping it gives up OCSP stapling — the only revocation signal
|
|
476
|
+
this package consumes — so pairing it with `trust.revocation: 'require-staple'` is refused at
|
|
477
|
+
configuration time rather than left to fail every connection on a certificate that was never asked
|
|
478
|
+
to carry a staple.
|
|
479
|
+
|
|
480
|
+
`tls.extraExtensions` takes pre-encoded extensions and is the only way to close it. They are ordered
|
|
481
|
+
like any other and reproduced on a HelloRetryRequest retry, because a second hello that changed its
|
|
482
|
+
extension set would be both malformed (RFC 8446 §4.1.2) and a signal in itself. Encoding them
|
|
483
|
+
correctly is the caller's job; this package does not parse what it did not build.
|
|
484
|
+
|
|
485
|
+
**Every offered cipher must be one this package can perform — unless you say otherwise.**
|
|
486
|
+
`tls.ciphers` used to be taken verbatim, so a list containing a CBC or RSA-key-exchange suite put a
|
|
487
|
+
number on the wire that a server could select, after which the AEAD layer had nothing to build and
|
|
488
|
+
the connection died mid-handshake. Such a list is refused at configuration time by default, and an
|
|
489
|
+
explicit `TLS_CHACHA20_POLY1305_SHA256` with no injected implementation is refused rather than
|
|
490
|
+
silently dropped — quietly presenting a different fingerprint from the one you asked for is the
|
|
491
|
+
worst outcome available to a package like this one.
|
|
492
|
+
|
|
493
|
+
But refusing outright is the wrong default to have no escape from, because **the restriction is
|
|
494
|
+
itself a fingerprint**:
|
|
495
|
+
|
|
496
|
+
| | suites offered | performable here |
|
|
497
|
+
| --- | --- | --- |
|
|
498
|
+
| curl 8.21.0 | 30 | 7 |
|
|
499
|
+
| Chromium | 15 | 7 |
|
|
500
|
+
|
|
501
|
+
A hello restricted to what can be honoured carries a cipher list less than half the length of any
|
|
502
|
+
real client's, and list length and contents are exactly what a JA3 hash reads. `tls.allowUnperformableCiphers`
|
|
503
|
+
offers the accurate list. The trade is narrow: the first unperformable suite sits at index 5 of
|
|
504
|
+
curl's list and 7 of Chromium's, behind the TLS 1.3 suites, so a server with 1.3 available never
|
|
505
|
+
reaches one. If a server does select one the handshake fails, and the error names the option rather
|
|
506
|
+
than reading like a defect here.
|
|
507
|
+
|
|
508
|
+
Most of the gap is not a missing feature. Sixteen of curl's twenty-three are CBC — MAC-then-encrypt,
|
|
509
|
+
which cannot be implemented without a Lucky13 padding oracle in JavaScript — and two more are RSA key
|
|
510
|
+
exchange with no forward secrecy. Both are refusals this package intends to keep. The three that
|
|
511
|
+
*are* implementable are the TLS 1.2 ChaCha20-Poly1305 suites, and they are not implemented yet.
|
|
512
|
+
|
|
456
513
|
Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
|
|
457
514
|
of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
|
|
458
515
|
§4.2.11 defines the binder transcript as the hello truncated just before the binders, which is a
|
|
@@ -548,6 +605,20 @@ new Client({ connect, proxy, maxBodyBytes: Infinity }); // or any number you h
|
|
|
548
605
|
The trade is deliberate: an unasked-for limit is discoverable the first time it bites, and names the
|
|
549
606
|
option in its error. An unasked-for OOM is neither.
|
|
550
607
|
|
|
608
|
+
#### The proxy sees a fingerprint too
|
|
609
|
+
|
|
610
|
+
`Proxy-Connection` is a pre-standard hop header that never reached a spec. The origin never sees it;
|
|
611
|
+
the proxy always does. Clients disagree — some send `keep-alive`, some `close`, some omit it — so for
|
|
612
|
+
anyone matching a client's behaviour *at the proxy* it is part of the fingerprint, and it used to be
|
|
613
|
+
hard-coded.
|
|
614
|
+
|
|
615
|
+
```js
|
|
616
|
+
new Client({ connect, proxy: { ...cfg, proxyConnection: 'close' } }); // or null to omit it
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
The default stays `keep-alive`, which avoids a class of proxy that closes the tunnel after one
|
|
620
|
+
request. Omitting the header is not the same as sending `close`.
|
|
621
|
+
|
|
551
622
|
### Trust — the `verify=` knob
|
|
552
623
|
|
|
553
624
|
```js
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tunnelfetch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
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",
|
|
@@ -122,9 +122,15 @@ function buildConnectRequest(proxy, target) {
|
|
|
122
122
|
if (proxy.username) {
|
|
123
123
|
lines.push(`Proxy-Authorization: Basic ${basicCredentials(proxy.username, proxy.password)}`);
|
|
124
124
|
}
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
|
|
125
|
+
// `Proxy-Connection` is a pre-standard hop header that never made it into a spec, and clients
|
|
126
|
+
// disagree about it: some send `keep-alive`, some send `close`, some omit it. The proxy sees this
|
|
127
|
+
// even though the origin never does, so for anyone matching a client's behaviour AT THE PROXY it
|
|
128
|
+
// is part of the fingerprint. `keep-alive` stays the default — it avoids a class of proxy that
|
|
129
|
+
// closes the tunnel after one request — but it is no longer fixed.
|
|
130
|
+
//
|
|
131
|
+
// `null` omits the header entirely, which is not the same as sending `close`.
|
|
132
|
+
const pc = proxy.proxyConnection === undefined ? 'keep-alive' : proxy.proxyConnection;
|
|
133
|
+
if (pc !== null) lines.push(`Proxy-Connection: ${pc}`);
|
|
128
134
|
return `${lines.join('\r\n')}\r\n\r\n`;
|
|
129
135
|
}
|
|
130
136
|
|
package/src/proxy/index.js
CHANGED
|
@@ -19,7 +19,13 @@ import { openSocks5 } from './socks5.js';
|
|
|
19
19
|
* @typedef {(addr: {hostname: string, port: number},
|
|
20
20
|
* opts?: {secureTransport?: 'off'|'on'|'starttls', allowHalfOpen?: boolean}) => Duplex} ConnectFn
|
|
21
21
|
* @typedef {{ protocol: 'http'|'https'|'socks5'|'socks5h', hostname: string, port: number,
|
|
22
|
-
* username?: string, password?: string
|
|
22
|
+
* username?: string, password?: string,
|
|
23
|
+
* proxyConnection?: string | null }} ProxyConfig
|
|
24
|
+
*
|
|
25
|
+
* `proxyConnection` sets the pre-standard `Proxy-Connection` header on a CONNECT request, or
|
|
26
|
+
* omits it entirely when null. Default 'keep-alive'. The origin never sees this header; the
|
|
27
|
+
* proxy does, so it belongs to whatever fingerprint the proxy is reading. Clients disagree —
|
|
28
|
+
* some send keep-alive, some close, some nothing — and omitting is not the same as 'close'.
|
|
23
29
|
*/
|
|
24
30
|
|
|
25
31
|
const DEFAULT_PORTS = { http: 8080, https: 443, socks5: 1080, socks5h: 1080 };
|
|
@@ -114,6 +120,10 @@ function normalise(cfg) {
|
|
|
114
120
|
port,
|
|
115
121
|
username: cfg.username || undefined,
|
|
116
122
|
password: cfg.password || undefined,
|
|
123
|
+
// Listed explicitly because this function REBUILDS the config rather than copying it, so a
|
|
124
|
+
// field not named here is dropped without a word. That is how `http2ConnectionWindow` came to
|
|
125
|
+
// be declared in a profile and read by nothing.
|
|
126
|
+
proxyConnection: cfg.proxyConnection,
|
|
117
127
|
});
|
|
118
128
|
}
|
|
119
129
|
|
package/src/tls/connect.js
CHANGED
|
@@ -137,7 +137,30 @@ function expectServerHello(msg, offers12) {
|
|
|
137
137
|
* @property {number[]} [groups] supported_groups, in preference order.
|
|
138
138
|
* @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
|
|
139
139
|
* supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
|
|
140
|
-
* @property {number[]} [ciphers] cipher suites to offer, in preference order.
|
|
140
|
+
* @property {number[]} [ciphers] cipher suites to offer, in preference order. By default every
|
|
141
|
+
* suite must be one this package can perform: an offer it cannot honour is a dead connection the
|
|
142
|
+
* moment a server selects it, so an unknown suite is refused here rather than on the wire.
|
|
143
|
+
* @property {number[]} [omitExtensions] extension types to leave out of the ClientHello, the
|
|
144
|
+
* subtractive counterpart to `extraExtensions`. `status_request` (5) is the one extension this
|
|
145
|
+
* package sends that curl does not, so an identity matching a sample without it needs this.
|
|
146
|
+
* Dropping it gives up OCSP stapling, which is the only revocation signal this package can
|
|
147
|
+
* consume — pairing it with `trust.revocation: 'require-staple'` is refused rather than left to
|
|
148
|
+
* fail every connection.
|
|
149
|
+
* @property {boolean} [allowUnperformableCiphers] offer suites this package cannot complete.
|
|
150
|
+
* For fingerprint fidelity only. Real clients offer far more than this package implements — curl
|
|
151
|
+
* 8.21.0 offers thirty against seven performable here, Chromium fifteen against seven — so a
|
|
152
|
+
* hello restricted to what it can honour carries a cipher list shorter than any real client's,
|
|
153
|
+
* which is exactly what a JA3 hash reads. With this set, a server that selects an unperformable
|
|
154
|
+
* suite fails the handshake; the first such suite sits behind the TLS 1.3 ones in both real
|
|
155
|
+
* lists, so a 1.3-capable server does not reach it. Knowingly trading a rare failure for an
|
|
156
|
+
* accurate fingerprint is a legitimate choice; making it silently is not.
|
|
157
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
|
|
158
|
+
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
159
|
+
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
160
|
+
* generate. Chromium sends several that it does not: `signed_certificate_timestamp` (18),
|
|
161
|
+
* `compress_certificate` (27), `session_ticket` (35), `application_settings` (17613) and ECH
|
|
162
|
+
* (65037). Supplying them here is the only way to close that gap, and it is the caller's job to
|
|
163
|
+
* encode them correctly — this package does not parse what it did not build.
|
|
141
164
|
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
142
165
|
* @property {boolean | number} [grease] send GREASE (RFC 8701) reserved values in the cipher list,
|
|
143
166
|
* the extension list (one at each end), supported_groups, supported_versions and key_share.
|
|
@@ -328,6 +351,47 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
328
351
|
];
|
|
329
352
|
if (!chacha) ciphers = ciphers.filter((c) => c !== CIPHER.TLS_CHACHA20_POLY1305_SHA256);
|
|
330
353
|
|
|
354
|
+
// A caller-supplied list was previously taken verbatim. That let a hello offer suites this
|
|
355
|
+
// package cannot perform — the CBC and RSA-key-exchange suites, most obviously — and the failure
|
|
356
|
+
// did not surface at configuration time. It surfaced after the ClientHello was on the wire, when
|
|
357
|
+
// a server took the offer up and the AEAD layer had nothing to build: a dead connection rather
|
|
358
|
+
// than a rejected config.
|
|
359
|
+
//
|
|
360
|
+
// profiles.js already states the principle for the profile system: "a fingerprint field this
|
|
361
|
+
// package cannot perform is an offer a server may take and then find unhonoured, which fails the
|
|
362
|
+
// connection rather than merely looking wrong". It applies just as much to `tls.ciphers`, and
|
|
363
|
+
// now does. CIPHER_PARAMS is the set this package knows how to key and seal.
|
|
364
|
+
//
|
|
365
|
+
// `allowUnperformableCiphers` opts out, and it exists because refusing outright was the wrong
|
|
366
|
+
// default to have no escape from. Every real client offers far more suites than this package
|
|
367
|
+
// implements — curl 8.21.0 offers thirty and seven are performable here; Chromium offers fifteen
|
|
368
|
+
// and seven are — so a hello restricted to what can be honoured has a cipher list shorter than
|
|
369
|
+
// any real client's, and that is itself what a JA3 hash reads. A caller matching a fingerprint
|
|
370
|
+
// may rationally prefer the accurate list: the first unperformable suite sits at index 5 of
|
|
371
|
+
// curl's and 7 of Chromium's, behind the TLS 1.3 suites, so a server with 1.3 available never
|
|
372
|
+
// reaches it. The risk is real but narrow, and it is the caller's to take knowingly.
|
|
373
|
+
if (options.ciphers && !options.allowUnperformableCiphers) {
|
|
374
|
+
const unperformable = ciphers.filter((c) => !CIPHER_PARAMS[c]);
|
|
375
|
+
if (unperformable.length) {
|
|
376
|
+
throw new TlsError(
|
|
377
|
+
codes.CONFIG_INVALID,
|
|
378
|
+
`tls.ciphers offers ${unperformable.map((c) => `0x${c.toString(16).padStart(4, '0')}`).join(', ')}, ` +
|
|
379
|
+
'which this package cannot perform. Offering a suite it cannot complete trades a ' +
|
|
380
|
+
'fingerprint match for a dead connection the moment a server selects one — see the ' +
|
|
381
|
+
'chrome profile, which drops the CBC and RSA-key-exchange suites for exactly this reason.',
|
|
382
|
+
{ unperformable },
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (!chacha && options.ciphers.includes(CIPHER.TLS_CHACHA20_POLY1305_SHA256)) {
|
|
386
|
+
throw new TlsError(
|
|
387
|
+
codes.CONFIG_INVALID,
|
|
388
|
+
'tls.ciphers offers TLS_CHACHA20_POLY1305_SHA256 but no ChaCha20 implementation was ' +
|
|
389
|
+
'supplied. Pass one through `ciphers: { chacha20 }`, or drop the suite from the list — ' +
|
|
390
|
+
'silently removing it would change the fingerprint you asked for without saying so.',
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
331
395
|
// --- resumption offer ----------------------------------------------------------------------
|
|
332
396
|
// Everything about the offered PSK that later steps need is derived once, up front: the Early
|
|
333
397
|
// Secret and binder key here (they depend only on the PSK), the binder itself per hello (it
|
|
@@ -372,6 +436,8 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
372
436
|
groups,
|
|
373
437
|
alpn,
|
|
374
438
|
ciphers,
|
|
439
|
+
extraExtensions: options.extraExtensions ?? [],
|
|
440
|
+
omitExtensions: options.omitExtensions ?? [],
|
|
375
441
|
versions,
|
|
376
442
|
extensionOrder: options.extensionOrder,
|
|
377
443
|
sigSchemes: options.sigSchemes,
|
|
@@ -192,6 +192,7 @@ export async function deriveSharedSecret(group, privateKey, peerKey, deps = {})
|
|
|
192
192
|
* @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
|
|
193
193
|
* @property {number[]} [versions] default [TLS13, TLS12]
|
|
194
194
|
* @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
|
|
195
|
+
* @property {number[]} [omitExtensions] extension types to leave out of the hello
|
|
195
196
|
* @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
|
|
196
197
|
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
197
198
|
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|
|
@@ -299,6 +300,7 @@ export function buildClientHello({
|
|
|
299
300
|
versions = [TLS13, TLS12],
|
|
300
301
|
extensionOrder = CURL_EXTENSION_ORDER,
|
|
301
302
|
extraExtensions = [],
|
|
303
|
+
omitExtensions = [],
|
|
302
304
|
psk = null,
|
|
303
305
|
grease = false,
|
|
304
306
|
randomBytes = defaultRandom,
|
|
@@ -341,11 +343,15 @@ export function buildClientHello({
|
|
|
341
343
|
'a resumption PSK was supplied but TLS 1.3 is not among the offered versions');
|
|
342
344
|
}
|
|
343
345
|
|
|
346
|
+
const omit = new Set(omitExtensions);
|
|
344
347
|
const extensionParts = [
|
|
345
348
|
encodeServerName(hostname),
|
|
346
|
-
//
|
|
347
|
-
// a stapled OCSP response is the only revocation signal this package can consume.
|
|
348
|
-
|
|
349
|
+
// Offered by default for either version: without it a server may not staple (RFC 6066 s8), and
|
|
350
|
+
// a stapled OCSP response is the only revocation signal this package can consume. It is also
|
|
351
|
+
// the one extension this package sends that curl does not, so an identity matching a sample
|
|
352
|
+
// without it needs a way to drop it — `omitExtensions` is the subtractive counterpart to
|
|
353
|
+
// `extraExtensions`, and dropping this one gives up stapling.
|
|
354
|
+
omit.has(EXTENSION.status_request) ? null : encodeStatusRequest(),
|
|
349
355
|
encodeSupportedGroups(g ? [g.take(), ...groups] : groups),
|
|
350
356
|
encodeSignatureAlgorithms(sigSchemes),
|
|
351
357
|
alpn.length ? encodeAlpn(alpn) : null,
|
|
@@ -589,7 +595,10 @@ export function negotiateCipher(serverHello, { offeredCiphers, version }) {
|
|
|
589
595
|
if (!params) {
|
|
590
596
|
throw new TlsUnsupportedError(
|
|
591
597
|
codes.TLS_CIPHER_UNSUPPORTED,
|
|
592
|
-
`cipher suite ${hex16(suite)}
|
|
598
|
+
`cipher suite ${hex16(suite)} was offered but this package cannot perform it, so the ` +
|
|
599
|
+
'handshake cannot continue. Either the offer list has a bug, or `allowUnperformableCiphers` ' +
|
|
600
|
+
'was set to match a real client\'s cipher list and a server has selected one of the suites ' +
|
|
601
|
+
'that choice knowingly put on the wire.',
|
|
593
602
|
{ cipherSuite: suite },
|
|
594
603
|
);
|
|
595
604
|
}
|
package/src/tls/handshake.js
CHANGED
|
@@ -246,7 +246,15 @@ export async function continueTls13(ctx) {
|
|
|
246
246
|
grease: options.grease ?? false,
|
|
247
247
|
random: hello.clientRandom,
|
|
248
248
|
legacySessionId: hello.legacySessionId,
|
|
249
|
-
|
|
249
|
+
// The caller's extras must be reproduced too, for the same reason the order is: s4.1.2 does
|
|
250
|
+
// not permit a second hello to change its extension SET, and a retry that quietly dropped
|
|
251
|
+
// them would present one fingerprint on the first flight and a different one on the second —
|
|
252
|
+
// a difference that is itself a signal.
|
|
253
|
+
omitExtensions: options.omitExtensions ?? [],
|
|
254
|
+
extraExtensions: [
|
|
255
|
+
...(options.extraExtensions ?? []),
|
|
256
|
+
...(cookie ? [cookieExtension(cookie)] : []),
|
|
257
|
+
],
|
|
250
258
|
psk: offeredPsk && {
|
|
251
259
|
identity: offeredPsk.identity,
|
|
252
260
|
obfuscatedTicketAge: offeredPsk.obfuscatedTicketAge(),
|
package/src/transport.js
CHANGED
|
@@ -124,6 +124,20 @@ export async function openConnection({
|
|
|
124
124
|
}) {
|
|
125
125
|
const target = targetFromUrl(url);
|
|
126
126
|
const proxyConfig = parseProxy(proxy);
|
|
127
|
+
|
|
128
|
+
// Dropping status_request gives up OCSP stapling, and stapling is the only revocation signal this
|
|
129
|
+
// package consumes. Combined with `require-staple` that is not a weakened check, it is a policy
|
|
130
|
+
// that can never be satisfied: every connection would fail with OCSP_REQUIRED, on a certificate
|
|
131
|
+
// that was never asked to carry a staple. Refused here, where both settings are visible.
|
|
132
|
+
if ((tls?.omitExtensions ?? []).includes(5) && trust?.revocation === 'require-staple') {
|
|
133
|
+
throw new ConfigError(
|
|
134
|
+
codes.CONFIG_INVALID,
|
|
135
|
+
"tls.omitExtensions drops status_request (5) while trust.revocation is 'require-staple'. " +
|
|
136
|
+
'Without status_request a server is not asked to staple, so no staple can arrive and every ' +
|
|
137
|
+
'connection would fail. Keep the extension, or relax the revocation policy — the two are ' +
|
|
138
|
+
'a contradiction rather than a stricter setting.',
|
|
139
|
+
);
|
|
140
|
+
}
|
|
127
141
|
const owns = !deadlines;
|
|
128
142
|
const dl = deadlines ?? new DeadlineController({}, { signal });
|
|
129
143
|
|
package/types/proxy/index.d.ts
CHANGED
|
@@ -49,12 +49,19 @@ export type ConnectFn = (addr: {
|
|
|
49
49
|
secureTransport?: "off" | "on" | "starttls";
|
|
50
50
|
allowHalfOpen?: boolean;
|
|
51
51
|
}) => Duplex;
|
|
52
|
+
/**
|
|
53
|
+
* `proxyConnection` sets the pre-standard `Proxy-Connection` header on a CONNECT request, or
|
|
54
|
+
* omits it entirely when null. Default 'keep-alive'. The origin never sees this header; the
|
|
55
|
+
* proxy does, so it belongs to whatever fingerprint the proxy is reading. Clients disagree —
|
|
56
|
+
* some send keep-alive, some close, some nothing — and omitting is not the same as 'close'.
|
|
57
|
+
*/
|
|
52
58
|
export type ProxyConfig = {
|
|
53
59
|
protocol: "http" | "https" | "socks5" | "socks5h";
|
|
54
60
|
hostname: string;
|
|
55
61
|
port: number;
|
|
56
62
|
username?: string;
|
|
57
63
|
password?: string;
|
|
64
|
+
proxyConnection?: string | null;
|
|
58
65
|
};
|
|
59
66
|
import { openDirect } from './direct.js';
|
|
60
67
|
import { openHttpConnect } from './http-connect.js';
|
package/types/tls/connect.d.ts
CHANGED
|
@@ -13,7 +13,30 @@
|
|
|
13
13
|
* @property {number[]} [groups] supported_groups, in preference order.
|
|
14
14
|
* @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
|
|
15
15
|
* supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
|
|
16
|
-
* @property {number[]} [ciphers] cipher suites to offer, in preference order.
|
|
16
|
+
* @property {number[]} [ciphers] cipher suites to offer, in preference order. By default every
|
|
17
|
+
* suite must be one this package can perform: an offer it cannot honour is a dead connection the
|
|
18
|
+
* moment a server selects it, so an unknown suite is refused here rather than on the wire.
|
|
19
|
+
* @property {number[]} [omitExtensions] extension types to leave out of the ClientHello, the
|
|
20
|
+
* subtractive counterpart to `extraExtensions`. `status_request` (5) is the one extension this
|
|
21
|
+
* package sends that curl does not, so an identity matching a sample without it needs this.
|
|
22
|
+
* Dropping it gives up OCSP stapling, which is the only revocation signal this package can
|
|
23
|
+
* consume — pairing it with `trust.revocation: 'require-staple'` is refused rather than left to
|
|
24
|
+
* fail every connection.
|
|
25
|
+
* @property {boolean} [allowUnperformableCiphers] offer suites this package cannot complete.
|
|
26
|
+
* For fingerprint fidelity only. Real clients offer far more than this package implements — curl
|
|
27
|
+
* 8.21.0 offers thirty against seven performable here, Chromium fifteen against seven — so a
|
|
28
|
+
* hello restricted to what it can honour carries a cipher list shorter than any real client's,
|
|
29
|
+
* which is exactly what a JA3 hash reads. With this set, a server that selects an unperformable
|
|
30
|
+
* suite fails the handshake; the first such suite sits behind the TLS 1.3 ones in both real
|
|
31
|
+
* lists, so a 1.3-capable server does not reach it. Knowingly trading a rare failure for an
|
|
32
|
+
* accurate fingerprint is a legitimate choice; making it silently is not.
|
|
33
|
+
* @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
|
|
34
|
+
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
35
|
+
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
36
|
+
* generate. Chromium sends several that it does not: `signed_certificate_timestamp` (18),
|
|
37
|
+
* `compress_certificate` (27), `session_ticket` (35), `application_settings` (17613) and ECH
|
|
38
|
+
* (65037). Supplying them here is the only way to close that gap, and it is the caller's job to
|
|
39
|
+
* encode them correctly — this package does not parse what it did not build.
|
|
17
40
|
* @property {number[]} [sigSchemes] signature_algorithms to offer, in preference order.
|
|
18
41
|
* @property {boolean | number} [grease] send GREASE (RFC 8701) reserved values in the cipher list,
|
|
19
42
|
* the extension list (one at each end), supported_groups, supported_versions and key_share.
|
|
@@ -160,9 +183,41 @@ export type TlsOptions = {
|
|
|
160
183
|
*/
|
|
161
184
|
offerGroups?: number[] | undefined;
|
|
162
185
|
/**
|
|
163
|
-
* cipher suites to offer, in preference order.
|
|
186
|
+
* cipher suites to offer, in preference order. By default every
|
|
187
|
+
* suite must be one this package can perform: an offer it cannot honour is a dead connection the
|
|
188
|
+
* moment a server selects it, so an unknown suite is refused here rather than on the wire.
|
|
164
189
|
*/
|
|
165
190
|
ciphers?: number[] | undefined;
|
|
191
|
+
/**
|
|
192
|
+
* extension types to leave out of the ClientHello, the
|
|
193
|
+
* subtractive counterpart to `extraExtensions`. `status_request` (5) is the one extension this
|
|
194
|
+
* package sends that curl does not, so an identity matching a sample without it needs this.
|
|
195
|
+
* Dropping it gives up OCSP stapling, which is the only revocation signal this package can
|
|
196
|
+
* consume — pairing it with `trust.revocation: 'require-staple'` is refused rather than left to
|
|
197
|
+
* fail every connection.
|
|
198
|
+
*/
|
|
199
|
+
omitExtensions?: number[] | undefined;
|
|
200
|
+
/**
|
|
201
|
+
* offer suites this package cannot complete.
|
|
202
|
+
* For fingerprint fidelity only. Real clients offer far more than this package implements — curl
|
|
203
|
+
* 8.21.0 offers thirty against seven performable here, Chromium fifteen against seven — so a
|
|
204
|
+
* hello restricted to what it can honour carries a cipher list shorter than any real client's,
|
|
205
|
+
* which is exactly what a JA3 hash reads. With this set, a server that selects an unperformable
|
|
206
|
+
* suite fails the handshake; the first such suite sits behind the TLS 1.3 ones in both real
|
|
207
|
+
* lists, so a 1.3-capable server does not reach it. Knowingly trading a rare failure for an
|
|
208
|
+
* accurate fingerprint is a legitimate choice; making it silently is not.
|
|
209
|
+
*/
|
|
210
|
+
allowUnperformableCiphers?: boolean | undefined;
|
|
211
|
+
/**
|
|
212
|
+
* pre-encoded ClientHello extensions, appended before
|
|
213
|
+
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
214
|
+
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
215
|
+
* generate. Chromium sends several that it does not: `signed_certificate_timestamp` (18),
|
|
216
|
+
* `compress_certificate` (27), `session_ticket` (35), `application_settings` (17613) and ECH
|
|
217
|
+
* (65037). Supplying them here is the only way to close that gap, and it is the caller's job to
|
|
218
|
+
* encode them correctly — this package does not parse what it did not build.
|
|
219
|
+
*/
|
|
220
|
+
extraExtensions?: Uint8Array<ArrayBufferLike>[] | undefined;
|
|
166
221
|
/**
|
|
167
222
|
* signature_algorithms to offer, in preference order.
|
|
168
223
|
*/
|
|
@@ -32,7 +32,7 @@ export function generateKeyShare(group: number, deps?: import("./connect.js").Tl
|
|
|
32
32
|
* @returns {Promise<Uint8Array>} throws on any degenerate or malformed peer key
|
|
33
33
|
*/
|
|
34
34
|
export function deriveSharedSecret(group: number, privateKey: CryptoKey | import("./hybrid.js").HybridPrivate, peerKey: Uint8Array, deps?: import("./connect.js").TlsDeps): Promise<Uint8Array>;
|
|
35
|
-
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, psk, grease, randomBytes, }: {
|
|
35
|
+
export function buildClientHello({ hostname, keyShares, random, legacySessionId, ciphers, groups, sigSchemes, alpn, versions, extensionOrder, extraExtensions, omitExtensions, psk, grease, randomBytes, }: {
|
|
36
36
|
hostname: any;
|
|
37
37
|
keyShares: any;
|
|
38
38
|
random: any;
|
|
@@ -44,6 +44,7 @@ export function buildClientHello({ hostname, keyShares, random, legacySessionId,
|
|
|
44
44
|
versions?: number[] | undefined;
|
|
45
45
|
extensionOrder?: readonly number[] | undefined;
|
|
46
46
|
extraExtensions?: never[] | undefined;
|
|
47
|
+
omitExtensions?: never[] | undefined;
|
|
47
48
|
psk?: null | undefined;
|
|
48
49
|
grease?: boolean | undefined;
|
|
49
50
|
randomBytes?: ((n: any) => Uint8Array<any>) | undefined;
|
|
@@ -305,6 +306,7 @@ export function checkAlpn(extensions: Map<number, Uint8Array>, offeredAlpn: stri
|
|
|
305
306
|
* @property {string[]} [alpn] default ['http/1.1']; empty array omits the extension
|
|
306
307
|
* @property {number[]} [versions] default [TLS13, TLS12]
|
|
307
308
|
* @property {Uint8Array[]} [extraExtensions] pre-encoded, sent verbatim (the HRR cookie)
|
|
309
|
+
* @property {number[]} [omitExtensions] extension types to leave out of the hello
|
|
308
310
|
* @property {{ identity: Uint8Array, obfuscatedTicketAge: number, binderLen: number }} [psk]
|
|
309
311
|
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
310
312
|
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|
|
@@ -432,6 +434,10 @@ export type ClientHelloOptions = {
|
|
|
432
434
|
* pre-encoded, sent verbatim (the HRR cookie)
|
|
433
435
|
*/
|
|
434
436
|
extraExtensions?: Uint8Array<ArrayBufferLike>[] | undefined;
|
|
437
|
+
/**
|
|
438
|
+
* extension types to leave out of the hello
|
|
439
|
+
*/
|
|
440
|
+
omitExtensions?: number[] | undefined;
|
|
435
441
|
/**
|
|
436
442
|
* offer this resumption PSK. Encoded with a zeroed binder placeholder; the caller MUST derive
|
|
437
443
|
* the real binder over `message.subarray(0, truncatedLength)` and patch it in at
|