quico 0.4.0 → 0.8.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 +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/tls_bridge.js
CHANGED
|
@@ -12,16 +12,92 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
16
|
-
|
|
15
|
+
import {
|
|
16
|
+
TLSSession,
|
|
17
|
+
createSecureContext as _lemonCreateSecureContext,
|
|
18
|
+
certificateObject
|
|
19
|
+
} from 'lemon-tls';
|
|
20
|
+
import { X509Certificate } from 'node:crypto';
|
|
17
21
|
import { DEBUG, Emitter } from './utils.js';
|
|
22
|
+
import { rootCertificates as lemonRoots, parseEcdhCurve as lemonParseEcdhCurve, DEFAULT_SERVER_GROUPS as lemonServerGroups,
|
|
23
|
+
DEFAULT_SIGNATURE_SCHEMES as lemonSigSchemes } from 'lemon-tls';
|
|
24
|
+
|
|
25
|
+
// The platform trust store for a QUIC client given no `ca` — lemon-tls's own
|
|
26
|
+
// export, the list its TCP path uses (compat.js). Not node:tls: quico imports
|
|
27
|
+
// none of Node's network stack (test/integration.mjs checks).
|
|
28
|
+
function defaultRoots() {
|
|
29
|
+
return lemonRoots || [];
|
|
30
|
+
}
|
|
18
31
|
import { build_transport_params, parse_transport_params } from './transport.js';
|
|
19
32
|
import {
|
|
20
33
|
TLS_CIPHER_SUITES
|
|
21
34
|
} from './crypto.js';
|
|
22
35
|
|
|
23
36
|
|
|
24
|
-
|
|
37
|
+
// The transport parameters we send (RFC 9000 §18.2) — these, with the
|
|
38
|
+
// application's choices in place of the defaults for the ones it may set
|
|
39
|
+
// (options.transportParams, camelCase, RFC 9000's names):
|
|
40
|
+
// initialMaxData the connection's receive window (1 MB)
|
|
41
|
+
// initialMaxStreamDataBidiLocal a bidirectional stream we open (256 KB)
|
|
42
|
+
// initialMaxStreamDataBidiRemote one the peer opens (256 KB)
|
|
43
|
+
// initialMaxStreamDataUni a unidirectional stream the peer opens (128 KB)
|
|
44
|
+
// initialMaxStreamsBidi streams the peer may have open at once (100)
|
|
45
|
+
// initialMaxStreamsUni (100)
|
|
46
|
+
// maxIdleTimeout ms without a packet from the peer before the
|
|
47
|
+
// connection is closed (30000); 0: no limit.
|
|
48
|
+
// The effective one is the smaller of the two
|
|
49
|
+
// sides' (RFC 9000 §10.1)
|
|
50
|
+
// The windows slide as the application reads, keeping these sizes; the
|
|
51
|
+
// stream limits are raised as streams close, keeping these counts.
|
|
52
|
+
var SETTABLE_TRANSPORT_PARAMS = {
|
|
53
|
+
initialMaxData: 'initial_max_data',
|
|
54
|
+
initialMaxStreamDataBidiLocal: 'initial_max_stream_data_bidi_local',
|
|
55
|
+
initialMaxStreamDataBidiRemote: 'initial_max_stream_data_bidi_remote',
|
|
56
|
+
initialMaxStreamDataUni: 'initial_max_stream_data_uni',
|
|
57
|
+
initialMaxStreamsBidi: 'initial_max_streams_bidi',
|
|
58
|
+
initialMaxStreamsUni: 'initial_max_streams_uni',
|
|
59
|
+
maxIdleTimeout: 'max_idle_timeout',
|
|
60
|
+
disableActiveMigration: 'disable_active_migration'
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// A client's key-exchange groups by default: what node:tls offers (OpenSSL
|
|
64
|
+
// 3.5) and the browsers do — the hybrid post-quantum X25519MLKEM768 first,
|
|
65
|
+
// then X25519, P-256, P-384 — where node:crypto has ML-KEM (Node 24.7+);
|
|
66
|
+
// without it, the classical groups alone. lemon-tls's own client default is
|
|
67
|
+
// the same (session/ecdh.js); named here because the QUIC hello's context is
|
|
68
|
+
// set by this bridge. The hybrid share goes with an X25519 share beside it,
|
|
69
|
+
// so a server without it needs no retry.
|
|
70
|
+
var CLIENT_DEFAULT_GROUPS = (function () {
|
|
71
|
+
try { return lemonParseEcdhCurve('X25519MLKEM768:X25519:P-256:P-384'); }
|
|
72
|
+
catch (e) { return [29, 23, 24]; }
|
|
73
|
+
})();
|
|
74
|
+
|
|
75
|
+
function defaultTransportParams(overrides) {
|
|
76
|
+
var tp = baseTransportParams();
|
|
77
|
+
for (var name in (overrides || {})) {
|
|
78
|
+
if (!Object.prototype.hasOwnProperty.call(overrides, name) || overrides[name] == null) continue;
|
|
79
|
+
var key = SETTABLE_TRANSPORT_PARAMS[name];
|
|
80
|
+
if (!key) throw new TypeError('transportParams.' + name + ' is not a transport parameter that can be set');
|
|
81
|
+
var v = overrides[name];
|
|
82
|
+
if (key === 'disable_active_migration') {
|
|
83
|
+
if (typeof v !== 'boolean') throw new TypeError('transportParams.' + name + ' must be a boolean');
|
|
84
|
+
if (v) tp[key] = true; else delete tp[key];
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// Varints (§16); stream counts at most 2^60 (§4.6). A window of 0 would
|
|
88
|
+
// let the peer send nothing; a count of 0 for unidirectional streams
|
|
89
|
+
// would leave HTTP/3 no room for its own.
|
|
90
|
+
var max = key.indexOf('streams') >= 0 ? Math.pow(2, 60) : Number.MAX_SAFE_INTEGER;
|
|
91
|
+
var min = key === 'initial_max_streams_uni' ? 3 : ((key === 'initial_max_streams_bidi' || key === 'max_idle_timeout') ? 0 : 1);
|
|
92
|
+
if (typeof v !== 'number' || Math.floor(v) !== v || v < min || v > max) {
|
|
93
|
+
throw new RangeError('transportParams.' + name + ' must be an integer from ' + min + ' to ' + max);
|
|
94
|
+
}
|
|
95
|
+
tp[key] = v;
|
|
96
|
+
}
|
|
97
|
+
return tp;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function baseTransportParams() {
|
|
25
101
|
return {
|
|
26
102
|
max_udp_payload_size: 65527,
|
|
27
103
|
max_idle_timeout: 30000,
|
|
@@ -30,16 +106,51 @@ function defaultTransportParams() {
|
|
|
30
106
|
initial_max_stream_data_bidi_remote: 262144,
|
|
31
107
|
initial_max_stream_data_uni: 131072,
|
|
32
108
|
initial_max_streams_bidi: 100,
|
|
33
|
-
|
|
109
|
+
// HTTP/3 takes three of these for itself (control, QPACK encoder and
|
|
110
|
+
// decoder) and keeps them for the whole connection, and credit comes
|
|
111
|
+
// back only as the peer's streams close: with 3, a peer could open no
|
|
112
|
+
// stream of its own — no WebTransport stream, no MOQT. Browsers grant 100.
|
|
113
|
+
initial_max_streams_uni: 100,
|
|
34
114
|
ack_delay_exponent: 3,
|
|
35
115
|
max_ack_delay: 25,
|
|
36
|
-
|
|
116
|
+
// Active migration allowed (RFC 9000 §9): a client may move to another
|
|
117
|
+
// network — Wi-Fi to cellular — and go on in the same connection. Each
|
|
118
|
+
// new address is validated before more than three times what it sent
|
|
119
|
+
// goes to it (§8), with congestion and RTT state started over (§9.4),
|
|
120
|
+
// and addressed by a connection ID of its own (§9.5). It was disabled.
|
|
121
|
+
// transportParams.disableActiveMigration: true says no again.
|
|
37
122
|
active_connection_id_limit: 4,
|
|
38
|
-
max_datagram_frame_size: 65527
|
|
123
|
+
max_datagram_frame_size: 65527,
|
|
124
|
+
// RESET_STREAM_AT taken (draft-ietf-quic-reliable-stream-reset): a peer
|
|
125
|
+
// may reset a stream and still have its first bytes delivered — a
|
|
126
|
+
// WebTransport stream's header, a MOQT subgroup's.
|
|
127
|
+
reset_stream_at: true
|
|
39
128
|
};
|
|
40
129
|
}
|
|
41
130
|
|
|
42
131
|
|
|
132
|
+
// The server's transport parameters a client keeps for 0-RTT (RFC 9000
|
|
133
|
+
// §7.4.1; RFC 9221 §3 for max_datagram_frame_size): its limits on what may
|
|
134
|
+
// be sent before this handshake's parameters arrive.
|
|
135
|
+
var REMEMBERED_PARAMS = ['active_connection_id_limit', 'initial_max_data', 'initial_max_stream_data_bidi_local',
|
|
136
|
+
'initial_max_stream_data_bidi_remote', 'initial_max_stream_data_uni', 'initial_max_streams_bidi',
|
|
137
|
+
'initial_max_streams_uni', 'max_datagram_frame_size', 'reset_stream_at'];
|
|
138
|
+
function rememberedParams(tp) {
|
|
139
|
+
var out = {};
|
|
140
|
+
for (var i = 0; i < REMEMBERED_PARAMS.length; i++) {
|
|
141
|
+
var k = REMEMBERED_PARAMS[i];
|
|
142
|
+
// A flag (reset_stream_at, draft-ietf-quic-reliable-stream-reset §3) as 1 or 0.
|
|
143
|
+
out[k] = tp && typeof tp[k] === 'number' ? tp[k] : (tp && tp[k] === true ? 1 : 0);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// The fingerprint options with QUIC's transport character — see getJA4.
|
|
149
|
+
function quicOpts(opts) {
|
|
150
|
+
return Object.assign({ protocol: 'q' }, opts || {});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
|
|
43
154
|
function TLSBridge(options) {
|
|
44
155
|
options = options || {};
|
|
45
156
|
|
|
@@ -48,9 +159,28 @@ function TLSBridge(options) {
|
|
|
48
159
|
var handshakeSecretsEmitted = false;
|
|
49
160
|
var appSecretsEmitted = false;
|
|
50
161
|
var helloHandled = false;
|
|
162
|
+
// A client's: what it keeps with each session it saves — the server's
|
|
163
|
+
// transport parameters (tp), and the application's own (app: HTTP/3's
|
|
164
|
+
// SETTINGS) once it has them — for 0-RTT next time.
|
|
165
|
+
var sessionAppData = null;
|
|
166
|
+
function keepAppData(k, v) {
|
|
167
|
+
sessionAppData = Object.assign({}, sessionAppData || {});
|
|
168
|
+
sessionAppData[k] = v;
|
|
169
|
+
if (session && session.setSessionAppData) session.setSessionAppData(sessionAppData);
|
|
170
|
+
}
|
|
171
|
+
var serverHelloSent = false; // our parameters went out with it (EncryptedExtensions)
|
|
51
172
|
var isServer = !!options.isServer;
|
|
52
173
|
var originalDcid = options.originalDcid || new Uint8Array(0);
|
|
53
|
-
|
|
174
|
+
// The connection ID this endpoint chose for itself — the Source Connection
|
|
175
|
+
// ID of its long headers, echoed as initial_source_connection_id (RFC 9000
|
|
176
|
+
// §7.3). The server's is its own too, not the one the client picked.
|
|
177
|
+
var localCid = options.localCid || new Uint8Array(0);
|
|
178
|
+
// Server: the stateless reset token for localCid (RFC 9000 §10.3), from
|
|
179
|
+
// quic_server, which alone holds the key it is derived from.
|
|
180
|
+
var statelessResetToken = options.statelessResetToken || null;
|
|
181
|
+
// Server, after a Retry: the SCID of that Retry, echoed as
|
|
182
|
+
// retry_source_connection_id (RFC 9000 §7.3).
|
|
183
|
+
var retrySourceCid = options.retrySourceCid || null;
|
|
54
184
|
var hostname = options.hostname || null;
|
|
55
185
|
// Peer certificate verification. Defaults to true, matching node:tls — but
|
|
56
186
|
// until now the option was never plumbed through at all, so quico could not
|
|
@@ -60,6 +190,8 @@ function TLSBridge(options) {
|
|
|
60
190
|
// server4 / server6, so every implementation disables verification there).
|
|
61
191
|
var rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
62
192
|
var ca = options.ca || null;
|
|
193
|
+
// Server: its TLS options as tls_options.js reads them, or null.
|
|
194
|
+
var serverTls = isServer ? (options.tlsOptions || null) : null;
|
|
63
195
|
// ALPN protocol(s) to advertise. Default ['h3']; normalize string → array.
|
|
64
196
|
var supportedAlpns = (function () {
|
|
65
197
|
var a = options.alpn || ['h3'];
|
|
@@ -74,42 +206,98 @@ function TLSBridge(options) {
|
|
|
74
206
|
rejectUnauthorized: rejectUnauthorized
|
|
75
207
|
};
|
|
76
208
|
if (ca) sessionOpts.ca = ca;
|
|
209
|
+
// No `ca`: a client verifies against the platform roots. Without them
|
|
210
|
+
// lemon-tls has no trust anchors and checks only the name in the
|
|
211
|
+
// certificate — a self-signed certificate for the right name passed,
|
|
212
|
+
// so an HTTP/3 request was open to a man-in-the-middle.
|
|
213
|
+
if (!isServer) sessionOpts.defaultCA = defaultRoots;
|
|
214
|
+
// Client: a certificate to send if the server asks for one
|
|
215
|
+
// (CertificateRequest) — tls.connect's key / cert / passphrase.
|
|
216
|
+
if (!isServer && options.key && options.cert) {
|
|
217
|
+
sessionOpts.key = options.key;
|
|
218
|
+
sessionOpts.cert = options.cert;
|
|
219
|
+
if (options.passphrase != null) sessionOpts.passphrase = options.passphrase;
|
|
220
|
+
}
|
|
221
|
+
// Session tickets (server). The keys come from the server, one set for
|
|
222
|
+
// all its connections: without them lemon-tls draws a fresh key per
|
|
223
|
+
// session, so every ticket was sealed with a key that died with its
|
|
224
|
+
// connection and no returning client could ever resume. ticketData is
|
|
225
|
+
// the application's (see lemon-tls): carried in the ticket, returned by
|
|
226
|
+
// getTicketInfo() on the connection that presents it.
|
|
227
|
+
if (isServer && options.ticketKeys) sessionOpts.ticketKeys = options.ticketKeys;
|
|
228
|
+
if (isServer && options.ticketData) sessionOpts.ticketData = options.ticketData;
|
|
229
|
+
// Encrypted Client Hello (RFC 9849): lemon-tls decrypts the real
|
|
230
|
+
// ClientHello — SNI, ALPN, transport parameters — and the handshake
|
|
231
|
+
// runs on it; QUIC needs nothing of its own for it.
|
|
232
|
+
if (isServer && options.ech) sessionOpts.ech = options.ech;
|
|
233
|
+
// OCSP stapling: lemon-tls asks it when a client requests status, and
|
|
234
|
+
// staples the answer in the Certificate (RFC 8446 §4.4.2.1).
|
|
235
|
+
if (isServer && options.ocspRequest) sessionOpts.ocspRequest = options.ocspRequest;
|
|
236
|
+
// What the tickets are for beyond TLS: the QUIC version (RFC 9369 §5),
|
|
237
|
+
// set again once it is negotiated (setTicketContext).
|
|
238
|
+
if (isServer && options.ticketContext) sessionOpts.ticketContext = options.ticketContext;
|
|
239
|
+
// 0-RTT (RFC 9001 §4.6): tickets that allow it, and accepting it —
|
|
240
|
+
// max_early_data_size 0xffffffff, as QUIC requires (§4.6.1); under the
|
|
241
|
+
// transport parameters the client will remember (earlyDataContext:
|
|
242
|
+
// another set and the 0-RTT is refused, RFC 9000 §7.4.1).
|
|
243
|
+
if (isServer && options.earlyData) {
|
|
244
|
+
sessionOpts.earlyData = true;
|
|
245
|
+
sessionOpts.maxEarlyData = 0xffffffff;
|
|
246
|
+
if (options.earlyDataMaxAge != null) sessionOpts.earlyDataMaxAge = options.earlyDataMaxAge;
|
|
247
|
+
if (options.earlyDataReplay) sessionOpts.earlyDataReplay = options.earlyDataReplay;
|
|
248
|
+
sessionOpts.earlyDataContext = options.earlyDataContext || '';
|
|
249
|
+
}
|
|
250
|
+
// The server's TLS options (tls_options.js, read once by quic_server):
|
|
251
|
+
// the ticket lifetime, certificate compression and client certificates
|
|
252
|
+
// here; the suites, groups and signature schemes at 'hello' below.
|
|
253
|
+
if (isServer && serverTls) {
|
|
254
|
+
if (serverTls.sessionTimeout !== undefined) sessionOpts.ticketLifetime = serverTls.sessionTimeout;
|
|
255
|
+
if (serverTls.certificateCompression) sessionOpts.certificateCompression = serverTls.certificateCompression;
|
|
256
|
+
if (serverTls.honorCipherOrder) sessionOpts.honorCipherOrder = true;
|
|
257
|
+
if (serverTls.requestCert) {
|
|
258
|
+
sessionOpts.requestCert = true;
|
|
259
|
+
sessionOpts.rejectUnauthorized = serverTls.rejectUnauthorized;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
77
262
|
if (!isServer) {
|
|
263
|
+
// The server's certificate chain may come compressed (RFC 8879):
|
|
264
|
+
// brotli, zstd, zlib — what Chrome and Firefox offer. A server here
|
|
265
|
+
// compresses for any client that offers it (lemon-tls's default).
|
|
266
|
+
sessionOpts.certificateCompression = true;
|
|
78
267
|
sessionOpts.sessionId = new Uint8Array(0); // QUIC: no compatibility mode
|
|
79
268
|
if (hostname) sessionOpts.servername = hostname;
|
|
269
|
+
// A saved session to resume (lemon-tls offers it only where it may be
|
|
270
|
+
// used: the same server name, within its lifetime, and — when the
|
|
271
|
+
// server must be verified — one that was).
|
|
272
|
+
if (options.session) sessionOpts.session = options.session;
|
|
273
|
+
// A stapled OCSP response for the server's certificate (tls.connect's
|
|
274
|
+
// requestOCSP); it comes as 'OCSPResponse'.
|
|
275
|
+
if (options.requestOCSP) sessionOpts.requestOCSP = true;
|
|
276
|
+
// Encrypted Client Hello (RFC 9849): the server's ECHConfigList — the
|
|
277
|
+
// real ClientHello, its name and transport parameters, goes
|
|
278
|
+
// encrypted — or GREASE ECH without one. lemon-tls builds both hellos.
|
|
279
|
+
if (options.ech) sessionOpts.ech = options.ech;
|
|
280
|
+
if (options.echGrease) sessionOpts.echGrease = true;
|
|
281
|
+
// 0-RTT with a session whose ticket allows it.
|
|
282
|
+
if (options.earlyData) sessionOpts.earlyData = true;
|
|
80
283
|
}
|
|
81
284
|
session = new TLSSession(sessionOpts);
|
|
82
285
|
setupSessionEvents();
|
|
83
286
|
|
|
84
287
|
// Client: set ALPN + transport params NOW, before setTimeout builds ClientHello
|
|
85
288
|
if (!isServer) {
|
|
86
|
-
var
|
|
87
|
-
tp.initial_source_connection_id = localCid;
|
|
88
|
-
var quicParams = build_transport_params(tp);
|
|
289
|
+
var quicParams = localQuicParams();
|
|
89
290
|
|
|
90
291
|
session.set_context({
|
|
91
292
|
local_supported_versions: [0x0304],
|
|
92
293
|
local_supported_alpns: supportedAlpns,
|
|
93
|
-
local_supported_groups:
|
|
94
|
-
local_supported_cipher_suites: [0x1301, 0x1302],
|
|
294
|
+
local_supported_groups: clientGroups(),
|
|
295
|
+
local_supported_cipher_suites: [0x1301, 0x1302, 0x1303],
|
|
95
296
|
local_extensions: [{ type: 0x39, data: quicParams }],
|
|
96
|
-
// Signature schemes offered
|
|
97
|
-
// server
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
// 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
|
|
101
|
-
// outright (alert 40 handshake_failure — seen live against
|
|
102
|
-
// speed.cloudflare.com; claude.ai worked only because that zone
|
|
103
|
-
// also carries an RSA cert).
|
|
104
|
-
// 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
|
|
105
|
-
// §4.1.2 requires CH2 to match CH1 exactly — offering a different
|
|
106
|
-
// list in CH1 made every HRR flow non-compliant.
|
|
107
|
-
local_supported_signature_algorithms: [
|
|
108
|
-
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
109
|
-
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
|
110
|
-
0x0807, 0x0808, // ed25519, ed448
|
|
111
|
-
0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
|
|
112
|
-
],
|
|
297
|
+
// Signature schemes offered: lemon-tls's list, ML-DSA included (see
|
|
298
|
+
// the server's below). A pkcs1-only list here once had Cloudflare's
|
|
299
|
+
// ECDSA-only zones refuse the ClientHello (handshake_failure).
|
|
300
|
+
local_supported_signature_algorithms: (options.sigalgs && options.sigalgs.length) ? options.sigalgs.slice() : lemonSigSchemes.slice(),
|
|
113
301
|
});
|
|
114
302
|
helloHandled = true; // ClientHello will be generated from this set_context
|
|
115
303
|
}
|
|
@@ -128,6 +316,45 @@ function TLSBridge(options) {
|
|
|
128
316
|
if (data && data.length > 0) session.message(data);
|
|
129
317
|
}
|
|
130
318
|
|
|
319
|
+
// The groups a client offers: the application's (ecdhCurve), or the
|
|
320
|
+
// default — node:tls's and the browsers' (see CLIENT_DEFAULT_GROUPS).
|
|
321
|
+
function clientGroups() {
|
|
322
|
+
return (options.groups && options.groups.length) ? options.groups.slice() : CLIENT_DEFAULT_GROUPS.slice();
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Our transport parameters as they go into the handshake: the connection's
|
|
326
|
+
// (defaults and the application's) and the fields of our role.
|
|
327
|
+
function localQuicParams() {
|
|
328
|
+
var tp = Object.assign({}, options.transportParams || defaultTransportParams());
|
|
329
|
+
// version_information (RFC 9368): what the connection says about
|
|
330
|
+
// versions when its parameters go out — the client's first flight, the
|
|
331
|
+
// server's after it has chosen.
|
|
332
|
+
if (typeof options.versionInformation === 'function') tp.version_information = options.versionInformation();
|
|
333
|
+
// grease_quic_bit (RFC 9287): this side accepts packets with the fixed
|
|
334
|
+
// bit 0 — the parser never checks it. Not on a UDP port shared with
|
|
335
|
+
// other protocols (RFC 9443), which tells QUIC apart by that bit.
|
|
336
|
+
if (options.greaseQuicBit !== false) tp.grease_quic_bit = true;
|
|
337
|
+
// min_ack_delay (draft-ietf-quic-ack-frequency): ACK_FREQUENCY is taken.
|
|
338
|
+
if (typeof options.minAckDelay === 'number') tp.min_ack_delay = options.minAckDelay;
|
|
339
|
+
if (isServer) {
|
|
340
|
+
// RFC 9000 §7.3: the server echoes the DCID of the client's first
|
|
341
|
+
// Initial, and names the SCID it put in its own long headers.
|
|
342
|
+
tp.original_destination_connection_id = originalDcid;
|
|
343
|
+
tp.initial_source_connection_id = localCid;
|
|
344
|
+
// §10.3: the token a client will recognise our Stateless Reset by.
|
|
345
|
+
// Only a secret one: a client accepts any datagram ending in these 16
|
|
346
|
+
// bytes as our reset, so a value an attacker can know — the fixed one
|
|
347
|
+
// that stood here — let anyone end a client's connection with a
|
|
348
|
+
// single forged packet. Without one the parameter is left out.
|
|
349
|
+
if (statelessResetToken) tp.stateless_reset_token = statelessResetToken;
|
|
350
|
+
if (retrySourceCid) tp.retry_source_connection_id = retrySourceCid;
|
|
351
|
+
} else {
|
|
352
|
+
// Client: initial_source_connection_id = our SCID (not DCID!)
|
|
353
|
+
tp.initial_source_connection_id = localCid;
|
|
354
|
+
}
|
|
355
|
+
return build_transport_params(tp);
|
|
356
|
+
}
|
|
357
|
+
|
|
131
358
|
function setupSessionEvents() {
|
|
132
359
|
|
|
133
360
|
// Peer's QUIC transport parameters ride in the TLS quic_transport_parameters
|
|
@@ -146,6 +373,10 @@ function TLSBridge(options) {
|
|
|
146
373
|
var parsed = parse_transport_params(e.data, 0);
|
|
147
374
|
peerParamsEmitted = true;
|
|
148
375
|
if (DEBUG) console.log('[tls] peer transport params parsed (' + e.data.length + 'B)');
|
|
376
|
+
// A client keeps, with the sessions this connection saves, the
|
|
377
|
+
// server's parameters that 0-RTT must stay within (RFC 9000
|
|
378
|
+
// §7.4.1) — given back with the early secret next time.
|
|
379
|
+
if (!isServer) keepAppData('tp', rememberedParams(parsed));
|
|
149
380
|
ev.emit('peerTransportParams', parsed);
|
|
150
381
|
} catch (err) {
|
|
151
382
|
if (DEBUG) console.log('[tls] peer transport params parse failed: ' + err.message);
|
|
@@ -160,52 +391,50 @@ function TLSBridge(options) {
|
|
|
160
391
|
helloHandled = true;
|
|
161
392
|
if (DEBUG) console.log('[tls] hello — configuring TLS');
|
|
162
393
|
|
|
163
|
-
|
|
164
|
-
var tp = defaultTransportParams();
|
|
165
|
-
|
|
166
|
-
if (isServer) {
|
|
167
|
-
// Server must echo original DCID and provide SCID
|
|
168
|
-
tp.original_destination_connection_id = originalDcid;
|
|
169
|
-
tp.initial_source_connection_id = originalDcid;
|
|
170
|
-
tp.stateless_reset_token = new Uint8Array(16).fill(0xab); // TODO: random per connection
|
|
171
|
-
} else {
|
|
172
|
-
// Client: initial_source_connection_id = our SCID (not DCID!)
|
|
173
|
-
tp.initial_source_connection_id = localCid;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
var quicParams = build_transport_params(tp);
|
|
394
|
+
var quicParams = localQuicParams();
|
|
177
395
|
|
|
178
396
|
var tlsContext = {
|
|
179
397
|
local_supported_versions: [0x0304],
|
|
180
398
|
local_supported_alpns: supportedAlpns,
|
|
181
|
-
|
|
182
|
-
|
|
399
|
+
// Server: X25519MLKEM768 (0x11ec, hybrid post-quantum) first — what
|
|
400
|
+
// browsers offer — then the classical groups. Client: classical only;
|
|
401
|
+
// see lemon-tls ecdh.js on why a client offers the hybrid only when
|
|
402
|
+
// asked.
|
|
403
|
+
local_supported_groups: (serverTls && serverTls.groups) || (isServer ? lemonServerGroups.slice() : clientGroups()),
|
|
404
|
+
// QUIC is TLS 1.3 (RFC 9001 §4.2): its three suites, ChaCha20 among
|
|
405
|
+
// them (crypto.js protects packets with either AEAD). The TLS 1.2
|
|
406
|
+
// suites that were listed could never be chosen. The server's
|
|
407
|
+
// `ciphers`, narrowed to these, where it set them.
|
|
408
|
+
local_supported_cipher_suites: (serverTls && serverTls.cipherSuites) || [0x1301, 0x1302, 0x1303],
|
|
183
409
|
local_extensions: [
|
|
184
410
|
{ type: 0x39, data: quicParams }
|
|
185
411
|
],
|
|
186
412
|
// Signature schemes offered to the peer (client: in the ClientHello;
|
|
187
|
-
// server: in CertificateRequest)
|
|
188
|
-
// EdDSA
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
//
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
// §4.1.2 requires CH2 to match CH1 exactly — offering a different
|
|
196
|
-
// list in CH1 made every HRR flow non-compliant.
|
|
197
|
-
local_supported_signature_algorithms: [
|
|
198
|
-
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
199
|
-
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
|
200
|
-
0x0807, 0x0808, // ed25519, ed448
|
|
201
|
-
0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
|
|
202
|
-
],
|
|
413
|
+
// server: in CertificateRequest): lemon-tls's list, as over TCP —
|
|
414
|
+
// ECDSA, EdDSA, RSA-PSS, then ML-DSA where Node has it, the legacy
|
|
415
|
+
// schemes last (accepted on certificates in the chain, never used in
|
|
416
|
+
// a TLS 1.3 CertificateVerify). The short list kept here by hand had
|
|
417
|
+
// no ML-DSA: a server with an ML-DSA certificate could not be
|
|
418
|
+
// verified over QUIC. The HRR's second ClientHello repeats it exactly
|
|
419
|
+
// (RFC 8446 §4.1.2).
|
|
420
|
+
local_supported_signature_algorithms: (serverTls && serverTls.sigalgs) || lemonSigSchemes.slice(),
|
|
203
421
|
};
|
|
204
422
|
|
|
205
423
|
session.set_context(tlsContext);
|
|
206
424
|
});
|
|
207
425
|
|
|
208
426
|
session.on('message', function (epoch, seq, type, data) {
|
|
427
|
+
if (isServer && type === 'hello') serverHelloSent = true; // lemon-tls names the ServerHello so
|
|
428
|
+
// A TLS alert is never CRYPTO data in QUIC (RFC 9001 §4.8): it becomes
|
|
429
|
+
// a CONNECTION_CLOSE carrying 0x100 + the alert code. It was sent down
|
|
430
|
+
// the CRYPTO stream like a handshake message — two bytes the peer could
|
|
431
|
+
// only read as a broken handshake — while this side went on waiting:
|
|
432
|
+
// a certificate refused, a handshake failed, and the connection stayed
|
|
433
|
+
// open until the handshake timeout with nothing reported.
|
|
434
|
+
if (type === 'alert') {
|
|
435
|
+
if (data && data.length >= 2 && data[0] === 2) ev.emit('alert', data[1]);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
209
438
|
var quicEpoch;
|
|
210
439
|
if (epoch === 0) quicEpoch = 'initial';
|
|
211
440
|
else if (epoch === 1) quicEpoch = 'handshake';
|
|
@@ -239,6 +468,38 @@ function TLSBridge(options) {
|
|
|
239
468
|
ev.emit('secureConnect');
|
|
240
469
|
});
|
|
241
470
|
|
|
471
|
+
// 0-RTT: the client_early_traffic_secret — the client's to write with,
|
|
472
|
+
// as soon as its ClientHello offers early data (with what it saved last
|
|
473
|
+
// time: the server's parameters, `remembered`); the server's to read
|
|
474
|
+
// with, once it has decided to take it. Then the verdict ('earlyData').
|
|
475
|
+
session.on('earlySecret', function (secret, info) {
|
|
476
|
+
var cipher = info && info.cipher ? info.cipher : session.getCipher();
|
|
477
|
+
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
478
|
+
ev.emit('earlySecret', { secret: secret, cipher: cipher, hash: hashName,
|
|
479
|
+
remembered: info && info.appData && info.appData.tp ? info.appData.tp : null,
|
|
480
|
+
rememberedApp: info && info.appData && info.appData.app != null ? info.appData.app : null });
|
|
481
|
+
});
|
|
482
|
+
session.on('earlyData', function (accepted) { ev.emit('earlyData', accepted); });
|
|
483
|
+
|
|
484
|
+
// Why the handshake failed: a certificate refused, a signature that does
|
|
485
|
+
// not verify, a size limit. lemon-tls has already sent the alert (it
|
|
486
|
+
// arrives above as 'alert' and closes the connection); this is the reason.
|
|
487
|
+
// Nothing listened here, so the reason was lost and the caller saw only a
|
|
488
|
+
// connection that went away. The code is the one node:tls reports for the
|
|
489
|
+
// same failure (authorizationError, e.g. DEPTH_ZERO_SELF_SIGNED_CERT).
|
|
490
|
+
session.on('error', function (err) {
|
|
491
|
+
if (err && !err.code && session.authorizationError) err.code = session.authorizationError;
|
|
492
|
+
ev.emit('error', err);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
// Client: each NewSessionTicket, as the session to resume with (a
|
|
496
|
+
// Buffer, as node:tls gives it). lemon-tls emits it on a server too, for
|
|
497
|
+
// each ticket it issues — that is not ours to hand on.
|
|
498
|
+
if (!isServer) {
|
|
499
|
+
session.on('session', function (blob) { ev.emit('session', Buffer.from(blob)); });
|
|
500
|
+
session.on('OCSPResponse', function (r) { ev.emit('OCSPResponse', Buffer.from(r)); });
|
|
501
|
+
}
|
|
502
|
+
|
|
242
503
|
session.on('keyUpdate', function (info) {
|
|
243
504
|
ev.emit('keyUpdate', info.direction, info.secret);
|
|
244
505
|
});
|
|
@@ -282,14 +543,165 @@ function TLSBridge(options) {
|
|
|
282
543
|
return { local: secrets.localAppSecret, remote: secrets.remoteAppSecret, cipher: cipher, hash: hashName };
|
|
283
544
|
},
|
|
284
545
|
getCipher: function () { return session ? session.getCipher() : null; },
|
|
546
|
+
|
|
547
|
+
/** The ALPN protocol the handshake settled on, or null before it has. */
|
|
548
|
+
getALPN: function () { return session ? (session.getALPN() || null) : null; },
|
|
549
|
+
|
|
550
|
+
/** Encrypted Client Hello (server): null (not offered) | 'accepted' | 'rejected'. */
|
|
551
|
+
getEchStatus: function () { return session ? session.getEchStatus() : null; },
|
|
552
|
+
|
|
553
|
+
/** This connection's ticket: { data, resumed, offered } (lemon-tls
|
|
554
|
+
* TLSSession.getTicketInfo); null before the handshake has started. */
|
|
555
|
+
getTicketInfo: function () { return session && session.getTicketInfo ? session.getTicketInfo() : null; },
|
|
556
|
+
|
|
557
|
+
/** Server: replace our transport parameters before the ServerHello is
|
|
558
|
+
* built — connectionOptions answering after the ClientHello, while the
|
|
559
|
+
* handshake waits for it (see quic_connection). Returns whether it was
|
|
560
|
+
* still in time. */
|
|
561
|
+
updateTransportParams: function (tp) {
|
|
562
|
+
options.transportParams = tp;
|
|
563
|
+
if (!session || !helloHandled) return true; // taken at 'hello'
|
|
564
|
+
if (serverHelloSent) return false;
|
|
565
|
+
session.set_context({ local_extensions: [{ type: 0x39, data: localQuicParams() }] });
|
|
566
|
+
return true;
|
|
567
|
+
},
|
|
568
|
+
|
|
569
|
+
/** Server: the transport's own state in the tickets issued from now on
|
|
570
|
+
* (lemon-tls setTicketState — apart from the application's ticketData),
|
|
571
|
+
* and another ticket sent now to carry it (sendTicket). Careful Resume's
|
|
572
|
+
* path measurements go this way. Each returns whether it was done. */
|
|
573
|
+
/** Server: the context tickets are issued and accepted under (lemon-tls
|
|
574
|
+
* setTicketContext) — the negotiated QUIC version. */
|
|
575
|
+
setTicketContext: function (ctx) { if (session && session.setTicketContext) session.setTicketContext(ctx); },
|
|
576
|
+
/** Server: the context early data is accepted under (lemon-tls
|
|
577
|
+
* setEarlyDataContext) — changed with the transport parameters. */
|
|
578
|
+
/** Client: the application protocol's settings to keep with the
|
|
579
|
+
* sessions saved from now on (RFC 9114 §7.2.4.2), for 0-RTT. */
|
|
580
|
+
setRememberedApp: function (v) { if (!isServer) keepAppData('app', v); },
|
|
581
|
+
setEarlyDataContext: function (ctx) { options.earlyDataContext = ctx; if (session && session.setEarlyDataContext) session.setEarlyDataContext(ctx); },
|
|
582
|
+
setTicketState: function (st) { return !!(session && session.setTicketState && session.setTicketState(st)); },
|
|
583
|
+
sendTicket: function () { return !!(session && session.sendTicket && session.sendTicket()); },
|
|
584
|
+
|
|
585
|
+
/** Whether the handshake resumed a session (a ticket was accepted). */
|
|
586
|
+
isResumed: function () { return !!(session && session.isResumed); },
|
|
587
|
+
|
|
588
|
+
/** 'TLSv1.3' — as TLSSocket.getProtocol() words it; QUIC has no other. */
|
|
589
|
+
getProtocol: function () {
|
|
590
|
+
var v = session ? session.getVersion() : null;
|
|
591
|
+
return v === 0x0304 ? 'TLSv1.3' : v === 0x0303 ? 'TLSv1.2' : null;
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
/** { name, standardName, version } — TLSSocket.getCipher()'s shape. */
|
|
595
|
+
getCipherInfo: function () {
|
|
596
|
+
var code = session ? session.getCipher() : null;
|
|
597
|
+
if (code == null) return null;
|
|
598
|
+
var info = TLS_CIPHER_SUITES[code];
|
|
599
|
+
var protocol = this.getProtocol();
|
|
600
|
+
if (!info) return { name: '0x' + code.toString(16), standardName: 'unknown', version: protocol };
|
|
601
|
+
return {
|
|
602
|
+
name: info.name || info.cipher || '0x' + code.toString(16),
|
|
603
|
+
standardName: info.standardName || info.cipher || 'unknown',
|
|
604
|
+
version: protocol
|
|
605
|
+
};
|
|
606
|
+
},
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* The TLS fingerprints of this QUIC handshake: JA4 and JA5 of the
|
|
610
|
+
* ClientHello, JA4S of the ServerHello — { hash, raw }, or null.
|
|
611
|
+
*
|
|
612
|
+
* The same methods a TLSSocket has over TCP — the session computes them
|
|
613
|
+
* either way, and only the path out to the caller was missing. Over QUIC
|
|
614
|
+
* there is no socket object to hang them on, so they come from here.
|
|
615
|
+
*
|
|
616
|
+
* `protocol: 'q'` is not cosmetic: the first character encodes the
|
|
617
|
+
* transport, and the same client produces a different fingerprint over
|
|
618
|
+
* QUIC than over TCP. Reporting a 't' for a QUIC connection would file it
|
|
619
|
+
* under the wrong client entirely.
|
|
620
|
+
*/
|
|
621
|
+
getJA4: function (opts) { return session && session.getJA4 ? session.getJA4(quicOpts(opts)) : null; },
|
|
622
|
+
|
|
623
|
+
/** TLS keying material (RFC 8446 §7.5), as node:tls's exportKeyingMaterial(length, label, context) — or null before the handshake has the secret. */
|
|
624
|
+
exportKeyingMaterial: function (length, label, context) {
|
|
625
|
+
return session && session.exportKeyingMaterial ? session.exportKeyingMaterial(length, label, context) : null;
|
|
626
|
+
},
|
|
627
|
+
/** The peer's certificate chain, leaf first, as DER — or null. */
|
|
628
|
+
getPeerCertificateChain: function () {
|
|
629
|
+
var chain = session && session.getPeerCertificate ? session.getPeerCertificate() : null;
|
|
630
|
+
return chain ? chain.map(function (e) { return e.cert; }) : null;
|
|
631
|
+
},
|
|
632
|
+
/** The peer's certificate as node:tls's getPeerCertificate() describes
|
|
633
|
+
* it (lemon-tls certificateObject) — {} when it sent none, as Node
|
|
634
|
+
* answers. On a resumed connection, the one its ticket carried. */
|
|
635
|
+
getPeerCertificate: function () {
|
|
636
|
+
var chain = session && session.getPeerCertificate ? session.getPeerCertificate() : null;
|
|
637
|
+
return (chain && chain.length > 0 && certificateObject(chain[0].cert)) || {};
|
|
638
|
+
},
|
|
639
|
+
/** Whether the peer's certificate was verified (TLSSocket.authorized):
|
|
640
|
+
* false when it sent none. And why not (authorizationError). */
|
|
641
|
+
isAuthorized: function () { return !!(session && session.authorized); },
|
|
642
|
+
authorizationError: function () { return session && session.authorized ? null : ((session && session.authorizationError) || null); },
|
|
643
|
+
getJA5: function (opts) { return session && session.getJA5 ? session.getJA5(quicOpts(opts)) : null; },
|
|
644
|
+
getJA4S: function (opts) { return session && session.getJA4S ? session.getJA4S(quicOpts(opts)) : null; },
|
|
645
|
+
/** What the handshake settled — version, cipher, group, ALPN, resumed,
|
|
646
|
+
* and the certificate compression used each way — as a TLSSocket's
|
|
647
|
+
* getNegotiationResult() (lemon-tls); null before there is a session. */
|
|
648
|
+
getNegotiationResult: function () { return session && session.getNegotiationResult ? session.getNegotiationResult() : null; },
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* The name the client asked for in its ClientHello (SNI), or false —
|
|
652
|
+
* TLSSocket.servername, same values. A client dialling a bare IP address
|
|
653
|
+
* sends none (RFC 6066 §3 forbids an IP literal there).
|
|
654
|
+
*/
|
|
655
|
+
getServername: function () {
|
|
656
|
+
return (session && session.context && session.context.remote_sni) || false;
|
|
657
|
+
},
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* The certificate this connection presents — TLSSocket.getCertificate()'s
|
|
661
|
+
* shape: { subject, issuer, subjectaltname, valid_from, valid_to,
|
|
662
|
+
* fingerprint, fingerprint256, serialNumber, raw }, or {} when none. On a
|
|
663
|
+
* server, the one the SNI lookup chose; on a resumed connection, the one
|
|
664
|
+
* the lookup chose again for the same name.
|
|
665
|
+
*/
|
|
666
|
+
getCertificate: function () {
|
|
667
|
+
var chain = session && session.getCertificate ? session.getCertificate() : null;
|
|
668
|
+
if (!chain || chain.length === 0) return {};
|
|
669
|
+
try {
|
|
670
|
+
var der = chain[0].cert || chain[0];
|
|
671
|
+
var x509 = new X509Certificate(Buffer.isBuffer(der) ? der : Buffer.from(der));
|
|
672
|
+
return {
|
|
673
|
+
subject: x509.subject,
|
|
674
|
+
issuer: x509.issuer,
|
|
675
|
+
subjectaltname: x509.subjectAltName,
|
|
676
|
+
valid_from: x509.validFrom,
|
|
677
|
+
valid_to: x509.validTo,
|
|
678
|
+
fingerprint: x509.fingerprint,
|
|
679
|
+
fingerprint256: x509.fingerprint256,
|
|
680
|
+
serialNumber: x509.serialNumber,
|
|
681
|
+
raw: der
|
|
682
|
+
};
|
|
683
|
+
} catch (e) {
|
|
684
|
+
return {};
|
|
685
|
+
}
|
|
686
|
+
},
|
|
687
|
+
|
|
285
688
|
close: function () { if (session && session.close) session.close(); }
|
|
286
689
|
};
|
|
287
690
|
}
|
|
288
691
|
|
|
289
692
|
/**
|
|
290
693
|
* Drop-in replacement for tls.createSecureContext.
|
|
291
|
-
*
|
|
292
|
-
* context
|
|
694
|
+
*
|
|
695
|
+
* One context now serves every protocol. It used to build a second, node:tls
|
|
696
|
+
* context alongside the lemon-tls one, because HTTP/2 and HTTP/1.1 ran on
|
|
697
|
+
* node:http2 and node:https and those need node's own context object. Both are
|
|
698
|
+
* gone — h2_server and h1_server run on lemon-tls through tcp_listener — so
|
|
699
|
+
* the duplicate context, and the three-way bridge in server.js that had to
|
|
700
|
+
* work out which kind it was holding, go with them.
|
|
701
|
+
*
|
|
702
|
+
* The raw credentials are still kept on the context: an SNICallback may hand
|
|
703
|
+
* one to a layer that needs to rebuild it, and throwing away the inputs would
|
|
704
|
+
* make that impossible.
|
|
293
705
|
*/
|
|
294
706
|
function createSecureContext(options) {
|
|
295
707
|
var ctx = _lemonCreateSecureContext(options);
|
|
@@ -297,17 +709,10 @@ function createSecureContext(options) {
|
|
|
297
709
|
ctx._rawKey = options.key || null;
|
|
298
710
|
ctx._rawCert = options.cert || null;
|
|
299
711
|
ctx._rawCa = options.ca || null;
|
|
300
|
-
try {
|
|
301
|
-
var nodeOpts = {};
|
|
302
|
-
if (options.key) nodeOpts.key = options.key;
|
|
303
|
-
if (options.cert) nodeOpts.cert = options.cert;
|
|
304
|
-
if (options.ca) nodeOpts.ca = options.ca;
|
|
305
|
-
ctx._nodeContext = nodeTls.createSecureContext(nodeOpts);
|
|
306
|
-
} catch (e) {
|
|
307
|
-
ctx._nodeContext = null;
|
|
308
|
-
}
|
|
309
712
|
}
|
|
310
713
|
return ctx;
|
|
311
714
|
}
|
|
312
715
|
|
|
313
|
-
|
|
716
|
+
// defaultTransportParams is exported so quic_connection raises its stream
|
|
717
|
+
// limits from the same numbers it advertised, rather than a second copy.
|
|
718
|
+
export { TLSBridge, createSecureContext, defaultTransportParams, rememberedParams };
|