tunnelfetch 1.0.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/LICENSE +28 -0
- package/README.md +617 -0
- package/README.zh-CN.md +470 -0
- package/package.json +74 -0
- package/src/client/cookies.js +429 -0
- package/src/client/decode.js +346 -0
- package/src/client/redirect.js +249 -0
- package/src/client.js +704 -0
- package/src/errors.js +181 -0
- package/src/http1/chunked.js +289 -0
- package/src/http1/index.js +10 -0
- package/src/http1/request.js +143 -0
- package/src/http1/response.js +493 -0
- package/src/http2/connection.js +1170 -0
- package/src/http2/constants.js +129 -0
- package/src/http2/frames.js +291 -0
- package/src/http2/hpack.js +420 -0
- package/src/http2/huffman.js +203 -0
- package/src/http2/index.js +21 -0
- package/src/index.js +46 -0
- package/src/pool.js +256 -0
- package/src/proxy/direct.js +62 -0
- package/src/proxy/http-connect.js +206 -0
- package/src/proxy/index.js +197 -0
- package/src/proxy/socks5.js +344 -0
- package/src/tls/aead.js +263 -0
- package/src/tls/connect.js +407 -0
- package/src/tls/constants.js +334 -0
- package/src/tls/extensions.js +376 -0
- package/src/tls/handshake-messages.js +901 -0
- package/src/tls/handshake.js +568 -0
- package/src/tls/handshake12.js +507 -0
- package/src/tls/index.js +44 -0
- package/src/tls/keyschedule.js +473 -0
- package/src/tls/record.js +872 -0
- package/src/tls/tickets.js +145 -0
- package/src/tls/transcript.js +101 -0
- package/src/tls/wire.js +224 -0
- package/src/transport.js +296 -0
- package/src/trust/der.js +551 -0
- package/src/trust/index.js +375 -0
- package/src/trust/name.js +235 -0
- package/src/trust/ocsp.js +759 -0
- package/src/trust/path.js +595 -0
- package/src/trust/roots.js +454 -0
- package/src/trust/x509.js +902 -0
- package/src/util/bytes.js +470 -0
- package/src/util/deadline.js +266 -0
- package/src/warmup-fixture.js +85 -0
- package/src/warmup.js +243 -0
- package/types/client/cookies.d.ts +159 -0
- package/types/client/decode.d.ts +54 -0
- package/types/client/redirect.d.ts +96 -0
- package/types/client.d.ts +323 -0
- package/types/errors.d.ts +141 -0
- package/types/http1/chunked.d.ts +48 -0
- package/types/http1/index.d.ts +3 -0
- package/types/http1/request.d.ts +44 -0
- package/types/http1/response.d.ts +183 -0
- package/types/http2/connection.d.ts +282 -0
- package/types/http2/constants.d.ts +95 -0
- package/types/http2/frames.d.ts +116 -0
- package/types/http2/hpack.d.ts +99 -0
- package/types/http2/huffman.d.ts +21 -0
- package/types/http2/index.d.ts +5 -0
- package/types/index.d.ts +17 -0
- package/types/pool.d.ts +135 -0
- package/types/proxy/direct.d.ts +26 -0
- package/types/proxy/http-connect.d.ts +37 -0
- package/types/proxy/index.d.ts +62 -0
- package/types/proxy/socks5.d.ts +47 -0
- package/types/tls/aead.d.ts +67 -0
- package/types/tls/connect.d.ts +280 -0
- package/types/tls/constants.d.ts +275 -0
- package/types/tls/extensions.d.ts +195 -0
- package/types/tls/handshake-messages.d.ts +430 -0
- package/types/tls/handshake.d.ts +90 -0
- package/types/tls/handshake12.d.ts +35 -0
- package/types/tls/index.d.ts +9 -0
- package/types/tls/keyschedule.d.ts +272 -0
- package/types/tls/record.d.ts +361 -0
- package/types/tls/tickets.d.ts +66 -0
- package/types/tls/transcript.d.ts +52 -0
- package/types/tls/wire.d.ts +106 -0
- package/types/transport.d.ts +222 -0
- package/types/trust/der.d.ts +239 -0
- package/types/trust/index.d.ts +194 -0
- package/types/trust/name.d.ts +33 -0
- package/types/trust/ocsp.d.ts +138 -0
- package/types/trust/path.d.ts +139 -0
- package/types/trust/roots.d.ts +36 -0
- package/types/trust/x509.d.ts +401 -0
- package/types/util/bytes.d.ts +183 -0
- package/types/util/deadline.d.ts +133 -0
- package/types/warmup-fixture.d.ts +11 -0
- package/types/warmup.d.ts +45 -0
package/types/pool.d.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything that decides whether two requests may share a socket. Mirrors what openConnection
|
|
3
|
+
* consumed to build the connection, because anything that influenced the connection must
|
|
4
|
+
* influence the key.
|
|
5
|
+
* @typedef {object} PoolKeyInput
|
|
6
|
+
* @property {string} scheme the URL protocol, colon included ('http:' | 'https:')
|
|
7
|
+
* @property {string} hostname
|
|
8
|
+
* @property {number} port
|
|
9
|
+
* @property {import('./proxy/index.js').ProxyConfig | null | undefined} proxy
|
|
10
|
+
* @property {import('./trust/index.js').TrustConfig | null | undefined} trust
|
|
11
|
+
* @property {import('./tls/connect.js').TlsOptions | null | undefined} tls
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* The trust configuration is part of the key. Two requests to the same origin under different
|
|
15
|
+
* verification policies must not share a connection — the peer was validated under one policy and
|
|
16
|
+
* silently reusing it satisfies the other policy without ever having checked it.
|
|
17
|
+
*
|
|
18
|
+
* @param {PoolKeyInput} input
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function poolKey({ scheme, hostname, port, proxy, trust, tls }: PoolKeyInput): string;
|
|
22
|
+
/**
|
|
23
|
+
* What the pool stores: the connection object openConnection resolves to. The pool itself only
|
|
24
|
+
* ever calls `close?.()`, but naming the real type keeps take() useful to a caller.
|
|
25
|
+
* @typedef {import('./transport.js').Connection} PooledConnection
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} PoolOptions
|
|
29
|
+
* @property {number} [maxPerKey] idle connections kept per key, default 6
|
|
30
|
+
* @property {number} [maxTotal] idle connections kept across all keys, default 24
|
|
31
|
+
*/
|
|
32
|
+
/**
|
|
33
|
+
* Running counters, never reset. `discarded` includes connections refused at release time;
|
|
34
|
+
* `evicted` counts victims pushed out by a newer release under a full pool.
|
|
35
|
+
* @typedef {object} PoolStats
|
|
36
|
+
* @property {number} hits
|
|
37
|
+
* @property {number} misses
|
|
38
|
+
* @property {number} released
|
|
39
|
+
* @property {number} discarded
|
|
40
|
+
* @property {number} evicted
|
|
41
|
+
*/
|
|
42
|
+
export class ConnectionPool {
|
|
43
|
+
/**
|
|
44
|
+
* @param {PoolOptions} [opts]
|
|
45
|
+
*/
|
|
46
|
+
constructor({ maxPerKey, maxTotal }?: PoolOptions);
|
|
47
|
+
/** @type {Map<string, Array<{conn: PooledConnection}>>} */
|
|
48
|
+
_idle: Map<string, Array<{
|
|
49
|
+
conn: PooledConnection;
|
|
50
|
+
}>>;
|
|
51
|
+
_total: number;
|
|
52
|
+
_maxPerKey: number;
|
|
53
|
+
_maxTotal: number;
|
|
54
|
+
_closed: boolean;
|
|
55
|
+
/** @type {PoolStats} */
|
|
56
|
+
stats: PoolStats;
|
|
57
|
+
get idleCount(): number;
|
|
58
|
+
/**
|
|
59
|
+
* Take an idle connection for `key`, or null. Most-recently-used first: it is likeliest live.
|
|
60
|
+
* @param {string} key
|
|
61
|
+
* @returns {PooledConnection | null}
|
|
62
|
+
*/
|
|
63
|
+
take(key: string): PooledConnection | null;
|
|
64
|
+
/**
|
|
65
|
+
* Offer a connection back. Callers must have proven the body reached its declared end; this
|
|
66
|
+
* method cannot verify that and deliberately does not pretend to — `eligible` is the caller's
|
|
67
|
+
* assertion, and the one place it is computed is the HTTP framing layer.
|
|
68
|
+
* Idle entries carry no age, deliberately. Ageing them out would only narrow the window in
|
|
69
|
+
* which a peer reaps a socket we still believe in, never close it — the peer can hang up at any
|
|
70
|
+
* instant, including the one after the check. What actually makes reuse safe is the recovery in
|
|
71
|
+
* sendAndReceive(): a reused connection that ends without producing one response byte is proof
|
|
72
|
+
* the request was never seen, and it is re-sent on a fresh connection. An age field would look
|
|
73
|
+
* like a second line of defence while being neither necessary nor sufficient.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} key
|
|
76
|
+
* @param {PooledConnection} conn
|
|
77
|
+
* @param {boolean} eligible
|
|
78
|
+
* @returns {boolean} whether the connection was retained
|
|
79
|
+
*/
|
|
80
|
+
release(key: string, conn: PooledConnection, eligible: boolean): boolean;
|
|
81
|
+
/**
|
|
82
|
+
* Close and forget one connection that must not be reused.
|
|
83
|
+
* @param {PooledConnection} conn
|
|
84
|
+
* @returns {Promise<void>}
|
|
85
|
+
*/
|
|
86
|
+
discard(conn: PooledConnection): Promise<void>;
|
|
87
|
+
/**
|
|
88
|
+
* Close everything. A Client that is done must call this or sockets leak for the isolate.
|
|
89
|
+
* @returns {Promise<void>}
|
|
90
|
+
*/
|
|
91
|
+
closeAll(): Promise<void>;
|
|
92
|
+
_assertOpen(): void;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Everything that decides whether two requests may share a socket. Mirrors what openConnection
|
|
96
|
+
* consumed to build the connection, because anything that influenced the connection must
|
|
97
|
+
* influence the key.
|
|
98
|
+
*/
|
|
99
|
+
export type PoolKeyInput = {
|
|
100
|
+
/**
|
|
101
|
+
* the URL protocol, colon included ('http:' | 'https:')
|
|
102
|
+
*/
|
|
103
|
+
scheme: string;
|
|
104
|
+
hostname: string;
|
|
105
|
+
port: number;
|
|
106
|
+
proxy: import("./proxy/index.js").ProxyConfig | null | undefined;
|
|
107
|
+
trust: import("./trust/index.js").TrustConfig | null | undefined;
|
|
108
|
+
tls: import("./tls/connect.js").TlsOptions | null | undefined;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* What the pool stores: the connection object openConnection resolves to. The pool itself only
|
|
112
|
+
* ever calls `close?.()`, but naming the real type keeps take() useful to a caller.
|
|
113
|
+
*/
|
|
114
|
+
export type PooledConnection = import("./transport.js").Connection;
|
|
115
|
+
export type PoolOptions = {
|
|
116
|
+
/**
|
|
117
|
+
* idle connections kept per key, default 6
|
|
118
|
+
*/
|
|
119
|
+
maxPerKey?: number | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* idle connections kept across all keys, default 24
|
|
122
|
+
*/
|
|
123
|
+
maxTotal?: number | undefined;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Running counters, never reset. `discarded` includes connections refused at release time;
|
|
127
|
+
* `evicted` counts victims pushed out by a newer release under a full pool.
|
|
128
|
+
*/
|
|
129
|
+
export type PoolStats = {
|
|
130
|
+
hits: number;
|
|
131
|
+
misses: number;
|
|
132
|
+
released: number;
|
|
133
|
+
discarded: number;
|
|
134
|
+
evicted: number;
|
|
135
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} DirectOptions
|
|
3
|
+
* @property {{ hostname: string, port: number }} target
|
|
4
|
+
* @property {import('./index.js').ConnectFn} connect injected socket factory
|
|
5
|
+
* @property {AbortSignal} [signal]
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Dial the target itself. Resolves with the raw socket duplex; a refused or failed dial throws
|
|
9
|
+
* ProxyError (PROXY_UNREACHABLE) quoting the runtime's own message, which is the best
|
|
10
|
+
* diagnostic a caller will get.
|
|
11
|
+
*
|
|
12
|
+
* @param {DirectOptions} args
|
|
13
|
+
* @returns {Promise<import('./index.js').Duplex>}
|
|
14
|
+
*/
|
|
15
|
+
export function openDirect({ target, connect, signal }: DirectOptions): Promise<import("./index.js").Duplex>;
|
|
16
|
+
export type DirectOptions = {
|
|
17
|
+
target: {
|
|
18
|
+
hostname: string;
|
|
19
|
+
port: number;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* injected socket factory
|
|
23
|
+
*/
|
|
24
|
+
connect: import("./index.js").ConnectFn;
|
|
25
|
+
signal?: AbortSignal | undefined;
|
|
26
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Establish a CONNECT tunnel through an http/https proxy. Resolves with the tunnel duplex;
|
|
3
|
+
* every refusal (non-2xx, 407 with or without credentials, malformed reply) throws a
|
|
4
|
+
* ProxyError naming what the proxy answered.
|
|
5
|
+
*
|
|
6
|
+
* @param {HttpConnectOptions} args
|
|
7
|
+
* @returns {Promise<ProxyTunnel>}
|
|
8
|
+
*/
|
|
9
|
+
export function openHttpConnect({ proxy, target, connect, signal, limits }: HttpConnectOptions): Promise<ProxyTunnel>;
|
|
10
|
+
/**
|
|
11
|
+
* The tunnel a proxy module hands back: the byte duplex plus the underlying socket, kept so a
|
|
12
|
+
* caller that must tear down the transport can reach past the wrapping streams.
|
|
13
|
+
*/
|
|
14
|
+
export type ProxyTunnel = import("./index.js").Duplex & {
|
|
15
|
+
socket: import("./index.js").Duplex;
|
|
16
|
+
};
|
|
17
|
+
export type HttpConnectOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* protocol 'http' or 'https'
|
|
20
|
+
*/
|
|
21
|
+
proxy: import("./index.js").ProxyConfig;
|
|
22
|
+
target: {
|
|
23
|
+
hostname: string;
|
|
24
|
+
port: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* injected socket factory
|
|
28
|
+
*/
|
|
29
|
+
connect: import("./index.js").ConnectFn;
|
|
30
|
+
signal?: AbortSignal | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* CONNECT reply head cap, default 32768
|
|
33
|
+
*/
|
|
34
|
+
limits?: {
|
|
35
|
+
maxProxyReplyBytes?: number;
|
|
36
|
+
} | undefined;
|
|
37
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalise a proxy spec. Accepts a URL string (`http://user:pass@host:8080`,
|
|
3
|
+
* `socks5://host:1080`) or an object.
|
|
4
|
+
*
|
|
5
|
+
* `socks5h` is accepted as an alias of `socks5` because that is the spelling curl popularised for
|
|
6
|
+
* "resolve names at the proxy" — which is the only mode this package implements, since the
|
|
7
|
+
* runtime gives us no resolver and remote resolution is also what avoids leaking the target to
|
|
8
|
+
* the local DNS path.
|
|
9
|
+
*
|
|
10
|
+
* @param {string | ProxyConfig | null | undefined} spec
|
|
11
|
+
* @returns {ProxyConfig | null}
|
|
12
|
+
*/
|
|
13
|
+
export function parseProxy(spec: string | ProxyConfig | null | undefined): ProxyConfig | null;
|
|
14
|
+
/**
|
|
15
|
+
* Open a byte tunnel to `target`, through `proxy` if given.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} args
|
|
18
|
+
* @param {ProxyConfig | string | null} [args.proxy] null/absent means a direct connection
|
|
19
|
+
* @param {{hostname: string, port: number}} args.target
|
|
20
|
+
* @param {ConnectFn} args.connect socket factory, injected
|
|
21
|
+
* @param {AbortSignal} [args.signal]
|
|
22
|
+
* @param {object} [args.limits]
|
|
23
|
+
* @returns {Promise<Duplex & { proxied: boolean }>}
|
|
24
|
+
*/
|
|
25
|
+
export function openTunnel({ proxy, target, connect, signal, limits }: {
|
|
26
|
+
proxy?: string | ProxyConfig | null | undefined;
|
|
27
|
+
target: {
|
|
28
|
+
hostname: string;
|
|
29
|
+
port: number;
|
|
30
|
+
};
|
|
31
|
+
connect: ConnectFn;
|
|
32
|
+
signal?: AbortSignal | undefined;
|
|
33
|
+
limits?: object | undefined;
|
|
34
|
+
}): Promise<Duplex & {
|
|
35
|
+
proxied: boolean;
|
|
36
|
+
}>;
|
|
37
|
+
/** Close a duplex without caring whether it was already gone. */
|
|
38
|
+
export function closeQuietly(duplex: any): Promise<void>;
|
|
39
|
+
export type Duplex = {
|
|
40
|
+
readable: ReadableStream<Uint8Array>;
|
|
41
|
+
writable: WritableStream<Uint8Array>;
|
|
42
|
+
opened?: Promise<unknown>;
|
|
43
|
+
close?: () => Promise<void>;
|
|
44
|
+
};
|
|
45
|
+
export type ConnectFn = (addr: {
|
|
46
|
+
hostname: string;
|
|
47
|
+
port: number;
|
|
48
|
+
}, opts?: {
|
|
49
|
+
secureTransport?: "off" | "on" | "starttls";
|
|
50
|
+
allowHalfOpen?: boolean;
|
|
51
|
+
}) => Duplex;
|
|
52
|
+
export type ProxyConfig = {
|
|
53
|
+
protocol: "http" | "https" | "socks5" | "socks5h";
|
|
54
|
+
hostname: string;
|
|
55
|
+
port: number;
|
|
56
|
+
username?: string;
|
|
57
|
+
password?: string;
|
|
58
|
+
};
|
|
59
|
+
import { openDirect } from './direct.js';
|
|
60
|
+
import { openHttpConnect } from './http-connect.js';
|
|
61
|
+
import { openSocks5 } from './socks5.js';
|
|
62
|
+
export { openDirect, openHttpConnect, openSocks5 };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} Socks5Options
|
|
3
|
+
* @property {import('./index.js').ProxyConfig} proxy credentials trigger RFC 1929 user/pass auth
|
|
4
|
+
* @property {{ hostname: string, port: number }} target
|
|
5
|
+
* @property {import('./index.js').ConnectFn} connect injected socket factory
|
|
6
|
+
* @property {AbortSignal} [signal]
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Establish a SOCKS5 tunnel. Resolves with the tunnel duplex; every refusal (no acceptable
|
|
10
|
+
* auth method, rejected credentials, non-zero reply code, unframeable reply) throws a
|
|
11
|
+
* ProxyError naming the exact wire value the proxy sent.
|
|
12
|
+
*
|
|
13
|
+
* @param {Socks5Options} args
|
|
14
|
+
* @returns {Promise<import('./http-connect.js').ProxyTunnel>}
|
|
15
|
+
*/
|
|
16
|
+
export function openSocks5({ proxy, target, connect, signal }: Socks5Options): Promise<import("./http-connect.js").ProxyTunnel>;
|
|
17
|
+
/**
|
|
18
|
+
* Encode DST.ADDR + DST.PORT. Prefers the domain form so the proxy resolves.
|
|
19
|
+
* @param {{ hostname: string, port: number }} target
|
|
20
|
+
* @returns {Uint8Array}
|
|
21
|
+
*/
|
|
22
|
+
export function encodeAddress(target: {
|
|
23
|
+
hostname: string;
|
|
24
|
+
port: number;
|
|
25
|
+
}): Uint8Array;
|
|
26
|
+
/**
|
|
27
|
+
* Minimal IPv6 text parser: `::` compression and a trailing embedded IPv4 are both real.
|
|
28
|
+
* Throws ConfigError on anything that does not expand to exactly 8 groups.
|
|
29
|
+
* @param {string} text
|
|
30
|
+
* @returns {Uint8Array} the 16 address bytes
|
|
31
|
+
*/
|
|
32
|
+
export function parseIpv6(text: string): Uint8Array;
|
|
33
|
+
export type Socks5Options = {
|
|
34
|
+
/**
|
|
35
|
+
* credentials trigger RFC 1929 user/pass auth
|
|
36
|
+
*/
|
|
37
|
+
proxy: import("./index.js").ProxyConfig;
|
|
38
|
+
target: {
|
|
39
|
+
hostname: string;
|
|
40
|
+
port: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* injected socket factory
|
|
44
|
+
*/
|
|
45
|
+
connect: import("./index.js").ConnectFn;
|
|
46
|
+
signal?: AbortSignal | undefined;
|
|
47
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TLS 1.3 per-record nonce: the 64-bit sequence number left-padded to the IV length, XORed
|
|
3
|
+
* with the static IV. Exported so the tests can pin the construction independently of a full
|
|
4
|
+
* encrypt round trip.
|
|
5
|
+
* @param {Uint8Array} iv
|
|
6
|
+
* @param {number | bigint} seq
|
|
7
|
+
* @returns {Uint8Array}
|
|
8
|
+
*/
|
|
9
|
+
export function buildNonce(iv: Uint8Array, seq: number | bigint): Uint8Array;
|
|
10
|
+
/**
|
|
11
|
+
* Record protection for one direction under one key. `encrypt` returns the encrypted record
|
|
12
|
+
* body ready for framing; `decrypt` either returns authenticated plaintext (with the inner
|
|
13
|
+
* content type under 1.3, the header type under 1.2) or throws TLS_RECORD — never garbage.
|
|
14
|
+
* @typedef {object} Aead
|
|
15
|
+
* @property {number} version
|
|
16
|
+
* @property {(seq: number | bigint, type: number, plaintext: Uint8Array,
|
|
17
|
+
* opts?: { padding?: number }) => Promise<Uint8Array>} encrypt
|
|
18
|
+
* @property {(seq: number | bigint, body: Uint8Array, header: Uint8Array)
|
|
19
|
+
* => Promise<{ type: number, plaintext: Uint8Array }>} decrypt
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} AeadOptions
|
|
23
|
+
* @property {number} [version] `TLS13` (default) or `TLS12`; picks nonce and AAD construction
|
|
24
|
+
* @property {number} cipher cipher suite id, must have CIPHER_PARAMS
|
|
25
|
+
* @property {Uint8Array} key
|
|
26
|
+
* @property {Uint8Array} iv the 12-byte static IV for TLS 1.3, the 4-byte implicit salt for
|
|
27
|
+
* TLS 1.2
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* Create record protection for one direction under one key. A new key (handshake -> application,
|
|
31
|
+
* KeyUpdate) means a new instance; sequence numbers restart with it.
|
|
32
|
+
*
|
|
33
|
+
* @param {AeadOptions} opts
|
|
34
|
+
* @returns {Promise<Aead>}
|
|
35
|
+
*/
|
|
36
|
+
export function createAead({ version, cipher, key, iv }: AeadOptions): Promise<Aead>;
|
|
37
|
+
/**
|
|
38
|
+
* Record protection for one direction under one key. `encrypt` returns the encrypted record
|
|
39
|
+
* body ready for framing; `decrypt` either returns authenticated plaintext (with the inner
|
|
40
|
+
* content type under 1.3, the header type under 1.2) or throws TLS_RECORD — never garbage.
|
|
41
|
+
*/
|
|
42
|
+
export type Aead = {
|
|
43
|
+
version: number;
|
|
44
|
+
encrypt: (seq: number | bigint, type: number, plaintext: Uint8Array, opts?: {
|
|
45
|
+
padding?: number;
|
|
46
|
+
}) => Promise<Uint8Array>;
|
|
47
|
+
decrypt: (seq: number | bigint, body: Uint8Array, header: Uint8Array) => Promise<{
|
|
48
|
+
type: number;
|
|
49
|
+
plaintext: Uint8Array;
|
|
50
|
+
}>;
|
|
51
|
+
};
|
|
52
|
+
export type AeadOptions = {
|
|
53
|
+
/**
|
|
54
|
+
* `TLS13` (default) or `TLS12`; picks nonce and AAD construction
|
|
55
|
+
*/
|
|
56
|
+
version?: number | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* cipher suite id, must have CIPHER_PARAMS
|
|
59
|
+
*/
|
|
60
|
+
cipher: number;
|
|
61
|
+
key: Uint8Array;
|
|
62
|
+
/**
|
|
63
|
+
* the 12-byte static IV for TLS 1.3, the 4-byte implicit salt for
|
|
64
|
+
* TLS 1.2
|
|
65
|
+
*/
|
|
66
|
+
iv: Uint8Array;
|
|
67
|
+
};
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A byte duplex: what every layer in this package consumes and produces.
|
|
3
|
+
* @typedef {{ readable: ReadableStream<Uint8Array>,
|
|
4
|
+
* writable: WritableStream<Uint8Array> }} ByteDuplex
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Handshake knobs. Every one of these narrows what is offered; none can widen it beyond what
|
|
8
|
+
* `constants.js` permits, so no option here can talk the client into a suite it refuses.
|
|
9
|
+
*
|
|
10
|
+
* @typedef {object} TlsOptions
|
|
11
|
+
* @property {number[]} [versions] versions to offer, from `TLS13` / `TLS12`. Default both.
|
|
12
|
+
* @property {string[]} [alpn] ALPN protocols to offer. Default `['http/1.1']`.
|
|
13
|
+
* @property {number[]} [groups] supported_groups, in preference order.
|
|
14
|
+
* @property {number[]} [offerGroups] groups to send an actual key_share for. Default the first
|
|
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
|
+
* @property {Uint8Array} [clientRandom] fixed ClientHello.random, for reproducible handshakes.
|
|
18
|
+
* @property {Uint8Array} [legacySessionId] fixed legacy_session_id, likewise.
|
|
19
|
+
* @property {boolean} [compatibilityCcs] send the middlebox-compatibility ChangeCipherSpec.
|
|
20
|
+
* Default true.
|
|
21
|
+
* @property {number} [maxHandshakeMessage] per-message cap; certificate chains dominate sizing.
|
|
22
|
+
* @property {number} [maxKeyUpdates] received KeyUpdates tolerated before it is called a flood.
|
|
23
|
+
* @property {number} [maxTranscriptBytes] cap on buffered handshake transcript.
|
|
24
|
+
* @property {ResumptionOffer} [psk] offer this resumption PSK (TLS 1.3 only; requires 1.3 in
|
|
25
|
+
* the offered versions). The server may decline, in which case the full handshake continues
|
|
26
|
+
* on this same connection — there is no reconnect at any layer.
|
|
27
|
+
* @property {(ticket: CapturedTicket) => void} [onSessionTicket] receive each NewSessionTicket
|
|
28
|
+
* this connection yields, already reduced to a usable PSK per RFC 8446 s7.1. Without this the
|
|
29
|
+
* tickets are read and discarded, exactly as before.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* A resumption PSK ready to offer, as produced by the ticket store from a CapturedTicket.
|
|
33
|
+
* `obfuscatedTicketAge` is a closure, not a number, because the age must be current at the
|
|
34
|
+
* moment each hello is BUILT — a HelloRetryRequest builds a second hello later — and because
|
|
35
|
+
* clock policy belongs to the store, not to this layer (which otherwise never reads a clock).
|
|
36
|
+
* `peer` rides along opaquely: it is whatever the original session's verifyPeer resolved with,
|
|
37
|
+
* and a resumed session (which has no Certificate message to verify) reports it as its own —
|
|
38
|
+
* sound only because the ticket store keys tickets by the full trust configuration.
|
|
39
|
+
* @typedef {object} ResumptionOffer
|
|
40
|
+
* @property {Uint8Array} identity the ticket
|
|
41
|
+
* @property {Uint8Array} psk
|
|
42
|
+
* @property {import('./keyschedule.js').ScheduleHash} hash the hash the PSK was minted under
|
|
43
|
+
* @property {() => number} obfuscatedTicketAge uint32 per RFC 8446 s4.2.11.1
|
|
44
|
+
* @property {object} [peer]
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* What a NewSessionTicket becomes by the time a caller sees it: the wire fields that govern
|
|
48
|
+
* offering (lifetime, age_add) plus the derived PSK and everything needed to check a future
|
|
49
|
+
* selection against it. `maxEarlyDataSize` is recorded for honesty but never acted on: 0-RTT
|
|
50
|
+
* is deliberately not implemented (see the driver's note).
|
|
51
|
+
* @typedef {object} CapturedTicket
|
|
52
|
+
* @property {Uint8Array} identity
|
|
53
|
+
* @property {Uint8Array} psk
|
|
54
|
+
* @property {import('./keyschedule.js').ScheduleHash} hash
|
|
55
|
+
* @property {number} cipherSuite
|
|
56
|
+
* @property {number} lifetimeSec
|
|
57
|
+
* @property {number} ageAdd
|
|
58
|
+
* @property {number | null} maxEarlyDataSize
|
|
59
|
+
* @property {string | null} alpnProtocol
|
|
60
|
+
* @property {object} peer
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Injectable nondeterminism. Supplying these makes a handshake byte-for-byte reproducible, which
|
|
64
|
+
* is what allows a recorded session to be replayed in an offline test.
|
|
65
|
+
* @typedef {object} TlsDeps
|
|
66
|
+
* @property {(n: number) => Uint8Array} [randomBytes]
|
|
67
|
+
* @property {(algorithm: object, group: number) => Promise<CryptoKeyPair>} [generateKeyPair]
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* What a completed handshake reports about itself.
|
|
71
|
+
* @typedef {object} TlsSessionInfo
|
|
72
|
+
* @property {number} version negotiated version, `0x0304` or `0x0303`
|
|
73
|
+
* @property {number} cipherSuite negotiated suite
|
|
74
|
+
* @property {number} group negotiated key-exchange group
|
|
75
|
+
* @property {string | null} alpnProtocol
|
|
76
|
+
* @property {string} hostname the identity the certificate was required to prove
|
|
77
|
+
* @property {boolean} [extendedMasterSecret] TLS 1.2 only: whether RFC 7627 was in effect
|
|
78
|
+
* @property {boolean} [resumed] TLS 1.3 only: the server accepted the offered resumption PSK,
|
|
79
|
+
* so no certificate crossed the wire on THIS connection; the identity is the one validated
|
|
80
|
+
* by the original handshake the ticket came from
|
|
81
|
+
*/
|
|
82
|
+
/**
|
|
83
|
+
* A live TLS session: a plaintext duplex plus what was negotiated to get it.
|
|
84
|
+
* @typedef {object} TlsSession
|
|
85
|
+
* @property {ReadableStream<Uint8Array>} readable
|
|
86
|
+
* @property {WritableStream<Uint8Array>} writable
|
|
87
|
+
* @property {import('./record.js').RecordLayer} record
|
|
88
|
+
* @property {object} peer whatever `verifyPeer` resolved with: the validated leaf
|
|
89
|
+
* @property {TlsSessionInfo} info
|
|
90
|
+
* @property {() => Promise<void>} close
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Run a TLS handshake over a byte duplex, negotiating the version, and return the plaintext
|
|
94
|
+
* duplex above it. The default offer is [TLS 1.3, TLS 1.2]; `options.versions` narrows it.
|
|
95
|
+
*
|
|
96
|
+
* @param {object} args
|
|
97
|
+
* @param {ByteDuplex} args.transport
|
|
98
|
+
* @param {string} args.hostname the identity the certificate must prove, and the SNI sent
|
|
99
|
+
* @param {import('./handshake.js').VerifyPeer} args.verifyPeer
|
|
100
|
+
* Must throw to reject. Resolves with the validated leaf; its SPKI is the only key either
|
|
101
|
+
* driver will accept a handshake signature from. Receives the peer's stapled OCSP response,
|
|
102
|
+
* when there is one, as its third argument.
|
|
103
|
+
* @param {TlsOptions} [args.options]
|
|
104
|
+
* @param {TlsDeps} [args.deps]
|
|
105
|
+
* @returns {Promise<TlsSession>}
|
|
106
|
+
*/
|
|
107
|
+
export function connectTls({ transport, hostname, verifyPeer, options, deps }: {
|
|
108
|
+
transport: ByteDuplex;
|
|
109
|
+
hostname: string;
|
|
110
|
+
verifyPeer: import("./handshake.js").VerifyPeer;
|
|
111
|
+
options?: TlsOptions | undefined;
|
|
112
|
+
deps?: TlsDeps | undefined;
|
|
113
|
+
}): Promise<TlsSession>;
|
|
114
|
+
/**
|
|
115
|
+
* A byte duplex: what every layer in this package consumes and produces.
|
|
116
|
+
*/
|
|
117
|
+
export type ByteDuplex = {
|
|
118
|
+
readable: ReadableStream<Uint8Array>;
|
|
119
|
+
writable: WritableStream<Uint8Array>;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Handshake knobs. Every one of these narrows what is offered; none can widen it beyond what
|
|
123
|
+
* `constants.js` permits, so no option here can talk the client into a suite it refuses.
|
|
124
|
+
*/
|
|
125
|
+
export type TlsOptions = {
|
|
126
|
+
/**
|
|
127
|
+
* versions to offer, from `TLS13` / `TLS12`. Default both.
|
|
128
|
+
*/
|
|
129
|
+
versions?: number[] | undefined;
|
|
130
|
+
/**
|
|
131
|
+
* ALPN protocols to offer. Default `['http/1.1']`.
|
|
132
|
+
*/
|
|
133
|
+
alpn?: string[] | undefined;
|
|
134
|
+
/**
|
|
135
|
+
* supported_groups, in preference order.
|
|
136
|
+
*/
|
|
137
|
+
groups?: number[] | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* groups to send an actual key_share for. Default the first
|
|
140
|
+
* supported group; a HelloRetryRequest recovers any other choice at the cost of a round trip.
|
|
141
|
+
*/
|
|
142
|
+
offerGroups?: number[] | undefined;
|
|
143
|
+
/**
|
|
144
|
+
* cipher suites to offer, in preference order.
|
|
145
|
+
*/
|
|
146
|
+
ciphers?: number[] | undefined;
|
|
147
|
+
/**
|
|
148
|
+
* fixed ClientHello.random, for reproducible handshakes.
|
|
149
|
+
*/
|
|
150
|
+
clientRandom?: Uint8Array<ArrayBufferLike> | undefined;
|
|
151
|
+
/**
|
|
152
|
+
* fixed legacy_session_id, likewise.
|
|
153
|
+
*/
|
|
154
|
+
legacySessionId?: Uint8Array<ArrayBufferLike> | undefined;
|
|
155
|
+
/**
|
|
156
|
+
* send the middlebox-compatibility ChangeCipherSpec.
|
|
157
|
+
* Default true.
|
|
158
|
+
*/
|
|
159
|
+
compatibilityCcs?: boolean | undefined;
|
|
160
|
+
/**
|
|
161
|
+
* per-message cap; certificate chains dominate sizing.
|
|
162
|
+
*/
|
|
163
|
+
maxHandshakeMessage?: number | undefined;
|
|
164
|
+
/**
|
|
165
|
+
* received KeyUpdates tolerated before it is called a flood.
|
|
166
|
+
*/
|
|
167
|
+
maxKeyUpdates?: number | undefined;
|
|
168
|
+
/**
|
|
169
|
+
* cap on buffered handshake transcript.
|
|
170
|
+
*/
|
|
171
|
+
maxTranscriptBytes?: number | undefined;
|
|
172
|
+
/**
|
|
173
|
+
* offer this resumption PSK (TLS 1.3 only; requires 1.3 in
|
|
174
|
+
* the offered versions). The server may decline, in which case the full handshake continues
|
|
175
|
+
* on this same connection — there is no reconnect at any layer.
|
|
176
|
+
*/
|
|
177
|
+
psk?: ResumptionOffer | undefined;
|
|
178
|
+
/**
|
|
179
|
+
* receive each NewSessionTicket
|
|
180
|
+
* this connection yields, already reduced to a usable PSK per RFC 8446 s7.1. Without this the
|
|
181
|
+
* tickets are read and discarded, exactly as before.
|
|
182
|
+
*/
|
|
183
|
+
onSessionTicket?: ((ticket: CapturedTicket) => void) | undefined;
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* A resumption PSK ready to offer, as produced by the ticket store from a CapturedTicket.
|
|
187
|
+
* `obfuscatedTicketAge` is a closure, not a number, because the age must be current at the
|
|
188
|
+
* moment each hello is BUILT — a HelloRetryRequest builds a second hello later — and because
|
|
189
|
+
* clock policy belongs to the store, not to this layer (which otherwise never reads a clock).
|
|
190
|
+
* `peer` rides along opaquely: it is whatever the original session's verifyPeer resolved with,
|
|
191
|
+
* and a resumed session (which has no Certificate message to verify) reports it as its own —
|
|
192
|
+
* sound only because the ticket store keys tickets by the full trust configuration.
|
|
193
|
+
*/
|
|
194
|
+
export type ResumptionOffer = {
|
|
195
|
+
/**
|
|
196
|
+
* the ticket
|
|
197
|
+
*/
|
|
198
|
+
identity: Uint8Array;
|
|
199
|
+
psk: Uint8Array;
|
|
200
|
+
/**
|
|
201
|
+
* the hash the PSK was minted under
|
|
202
|
+
*/
|
|
203
|
+
hash: import("./keyschedule.js").ScheduleHash;
|
|
204
|
+
/**
|
|
205
|
+
* uint32 per RFC 8446 s4.2.11.1
|
|
206
|
+
*/
|
|
207
|
+
obfuscatedTicketAge: () => number;
|
|
208
|
+
peer?: object | undefined;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* What a NewSessionTicket becomes by the time a caller sees it: the wire fields that govern
|
|
212
|
+
* offering (lifetime, age_add) plus the derived PSK and everything needed to check a future
|
|
213
|
+
* selection against it. `maxEarlyDataSize` is recorded for honesty but never acted on: 0-RTT
|
|
214
|
+
* is deliberately not implemented (see the driver's note).
|
|
215
|
+
*/
|
|
216
|
+
export type CapturedTicket = {
|
|
217
|
+
identity: Uint8Array;
|
|
218
|
+
psk: Uint8Array;
|
|
219
|
+
hash: import("./keyschedule.js").ScheduleHash;
|
|
220
|
+
cipherSuite: number;
|
|
221
|
+
lifetimeSec: number;
|
|
222
|
+
ageAdd: number;
|
|
223
|
+
maxEarlyDataSize: number | null;
|
|
224
|
+
alpnProtocol: string | null;
|
|
225
|
+
peer: object;
|
|
226
|
+
};
|
|
227
|
+
/**
|
|
228
|
+
* Injectable nondeterminism. Supplying these makes a handshake byte-for-byte reproducible, which
|
|
229
|
+
* is what allows a recorded session to be replayed in an offline test.
|
|
230
|
+
*/
|
|
231
|
+
export type TlsDeps = {
|
|
232
|
+
randomBytes?: ((n: number) => Uint8Array) | undefined;
|
|
233
|
+
generateKeyPair?: ((algorithm: object, group: number) => Promise<CryptoKeyPair>) | undefined;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* What a completed handshake reports about itself.
|
|
237
|
+
*/
|
|
238
|
+
export type TlsSessionInfo = {
|
|
239
|
+
/**
|
|
240
|
+
* negotiated version, `0x0304` or `0x0303`
|
|
241
|
+
*/
|
|
242
|
+
version: number;
|
|
243
|
+
/**
|
|
244
|
+
* negotiated suite
|
|
245
|
+
*/
|
|
246
|
+
cipherSuite: number;
|
|
247
|
+
/**
|
|
248
|
+
* negotiated key-exchange group
|
|
249
|
+
*/
|
|
250
|
+
group: number;
|
|
251
|
+
alpnProtocol: string | null;
|
|
252
|
+
/**
|
|
253
|
+
* the identity the certificate was required to prove
|
|
254
|
+
*/
|
|
255
|
+
hostname: string;
|
|
256
|
+
/**
|
|
257
|
+
* TLS 1.2 only: whether RFC 7627 was in effect
|
|
258
|
+
*/
|
|
259
|
+
extendedMasterSecret?: boolean | undefined;
|
|
260
|
+
/**
|
|
261
|
+
* TLS 1.3 only: the server accepted the offered resumption PSK,
|
|
262
|
+
* so no certificate crossed the wire on THIS connection; the identity is the one validated
|
|
263
|
+
* by the original handshake the ticket came from
|
|
264
|
+
*/
|
|
265
|
+
resumed?: boolean | undefined;
|
|
266
|
+
};
|
|
267
|
+
/**
|
|
268
|
+
* A live TLS session: a plaintext duplex plus what was negotiated to get it.
|
|
269
|
+
*/
|
|
270
|
+
export type TlsSession = {
|
|
271
|
+
readable: ReadableStream<Uint8Array>;
|
|
272
|
+
writable: WritableStream<Uint8Array>;
|
|
273
|
+
record: import("./record.js").RecordLayer;
|
|
274
|
+
/**
|
|
275
|
+
* whatever `verifyPeer` resolved with: the validated leaf
|
|
276
|
+
*/
|
|
277
|
+
peer: object;
|
|
278
|
+
info: TlsSessionInfo;
|
|
279
|
+
close: () => Promise<void>;
|
|
280
|
+
};
|