tunnelfetch 1.9.0 → 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 +48 -7
- 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 +28 -4
- package/src/tls/handshake-messages.js +13 -4
- package/src/tls/handshake.js +1 -0
- package/src/transport.js +14 -0
- package/types/proxy/index.d.ts +7 -0
- package/types/tls/connect.d.ts +40 -6
- package/types/tls/handshake-messages.d.ts +7 -1
package/README.md
CHANGED
|
@@ -470,18 +470,45 @@ extension **set** that is curl's. A test reads that gap straight out of the comm
|
|
|
470
470
|
capture and fails if it changes, so it cannot drift quietly — but it is a real difference and a
|
|
471
471
|
JA3/JA4 hash sees it.
|
|
472
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
|
+
|
|
473
480
|
`tls.extraExtensions` takes pre-encoded extensions and is the only way to close it. They are ordered
|
|
474
481
|
like any other and reproduced on a HelloRetryRequest retry, because a second hello that changed its
|
|
475
482
|
extension set would be both malformed (RFC 8446 §4.1.2) and a signal in itself. Encoding them
|
|
476
483
|
correctly is the caller's job; this package does not parse what it did not build.
|
|
477
484
|
|
|
478
|
-
**Every offered cipher must be one this package can perform
|
|
479
|
-
verbatim, so a list containing a CBC or RSA-key-exchange suite put a
|
|
480
|
-
server could select
|
|
481
|
-
mid-handshake. Such a list is
|
|
482
|
-
|
|
483
|
-
a different fingerprint from the one you asked for is the
|
|
484
|
-
this one.
|
|
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.
|
|
485
512
|
|
|
486
513
|
Extension order matters because JA3 and JA4 hash the extension list **in wire order**, so it is most
|
|
487
514
|
of what a fingerprinter reads. `pre_shared_key` is forced last whatever you ask for: RFC 8446
|
|
@@ -578,6 +605,20 @@ new Client({ connect, proxy, maxBodyBytes: Infinity }); // or any number you h
|
|
|
578
605
|
The trade is deliberate: an unasked-for limit is discoverable the first time it bites, and names the
|
|
579
606
|
option in its error. An unasked-for OOM is neither.
|
|
580
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
|
+
|
|
581
622
|
### Trust — the `verify=` knob
|
|
582
623
|
|
|
583
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,9 +137,23 @@ 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.
|
|
141
|
-
* one this package can perform
|
|
142
|
-
* server selects it, so an unknown suite is refused here rather than on the wire.
|
|
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.
|
|
143
157
|
* @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
|
|
144
158
|
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
145
159
|
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
@@ -347,7 +361,16 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
347
361
|
// package cannot perform is an offer a server may take and then find unhonoured, which fails the
|
|
348
362
|
// connection rather than merely looking wrong". It applies just as much to `tls.ciphers`, and
|
|
349
363
|
// now does. CIPHER_PARAMS is the set this package knows how to key and seal.
|
|
350
|
-
|
|
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) {
|
|
351
374
|
const unperformable = ciphers.filter((c) => !CIPHER_PARAMS[c]);
|
|
352
375
|
if (unperformable.length) {
|
|
353
376
|
throw new TlsError(
|
|
@@ -414,6 +437,7 @@ async function drive({ record, hostname, verifyPeer, options, deps, versions })
|
|
|
414
437
|
alpn,
|
|
415
438
|
ciphers,
|
|
416
439
|
extraExtensions: options.extraExtensions ?? [],
|
|
440
|
+
omitExtensions: options.omitExtensions ?? [],
|
|
417
441
|
versions,
|
|
418
442
|
extensionOrder: options.extensionOrder,
|
|
419
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
|
@@ -250,6 +250,7 @@ export async function continueTls13(ctx) {
|
|
|
250
250
|
// not permit a second hello to change its extension SET, and a retry that quietly dropped
|
|
251
251
|
// them would present one fingerprint on the first flight and a different one on the second —
|
|
252
252
|
// a difference that is itself a signal.
|
|
253
|
+
omitExtensions: options.omitExtensions ?? [],
|
|
253
254
|
extraExtensions: [
|
|
254
255
|
...(options.extraExtensions ?? []),
|
|
255
256
|
...(cookie ? [cookieExtension(cookie)] : []),
|
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,9 +13,23 @@
|
|
|
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.
|
|
17
|
-
* one this package can perform
|
|
18
|
-
* server selects it, so an unknown suite is refused here rather than on the wire.
|
|
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.
|
|
19
33
|
* @property {Uint8Array[]} [extraExtensions] pre-encoded ClientHello extensions, appended before
|
|
20
34
|
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
21
35
|
* exists and sorts that — so ordering alone cannot produce an extension this package does not
|
|
@@ -169,11 +183,31 @@ export type TlsOptions = {
|
|
|
169
183
|
*/
|
|
170
184
|
offerGroups?: number[] | undefined;
|
|
171
185
|
/**
|
|
172
|
-
* cipher suites to offer, in preference order.
|
|
173
|
-
* one this package can perform
|
|
174
|
-
* server selects it, so an unknown suite is refused here rather than on the wire.
|
|
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.
|
|
175
189
|
*/
|
|
176
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;
|
|
177
211
|
/**
|
|
178
212
|
* pre-encoded ClientHello extensions, appended before
|
|
179
213
|
* ordering. `extensionOrder` can only arrange extensions that were BUILT — it filters to what
|
|
@@ -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
|