quico 0.1.3 → 0.3.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 +381 -216
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +49 -23
- package/package.json +83 -60
- package/src/agent.js +283 -0
- package/src/client.js +444 -0
- package/src/crypto.js +534 -0
- package/src/h3.js +920 -0
- package/src/h3_client.js +332 -0
- package/src/h3_server.js +392 -0
- package/src/quic_connection.js +2338 -0
- package/src/quic_server.js +314 -0
- package/src/quic_socket.js +76 -0
- package/src/server.js +354 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +273 -0
- package/src/transport.js +667 -0
- package/src/utils.js +345 -0
- package/src/webtransport.js +655 -0
- package/CHANGELOG.md +0 -31
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -23
- package/h3_server.js +0 -416
- package/h3_socket.js +0 -473
- package/libs/crypto.js +0 -1841
- package/libs/h3.js +0 -1461
- package/libs/utils.js +0 -404
- package/quic_socket.js +0 -1550
- package/quico.svg +0 -1
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* quico: HTTP/3 and QUIC implementation for Node.js
|
|
3
|
+
* Copyright 2025 colocohen
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at:
|
|
8
|
+
*
|
|
9
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* This file is part of the open-source project hosted at:
|
|
12
|
+
* https://github.com/colocohen/quico
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { TLSSession, createSecureContext as _lemonCreateSecureContext } from 'lemon-tls';
|
|
16
|
+
import nodeTls from 'node:tls';
|
|
17
|
+
import { DEBUG, Emitter } from './utils.js';
|
|
18
|
+
import { build_transport_params, parse_transport_params } from './transport.js';
|
|
19
|
+
import {
|
|
20
|
+
TLS_CIPHER_SUITES
|
|
21
|
+
} from './crypto.js';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
function defaultTransportParams() {
|
|
25
|
+
return {
|
|
26
|
+
max_udp_payload_size: 65527,
|
|
27
|
+
max_idle_timeout: 30000,
|
|
28
|
+
initial_max_data: 1048576,
|
|
29
|
+
initial_max_stream_data_bidi_local: 262144,
|
|
30
|
+
initial_max_stream_data_bidi_remote: 262144,
|
|
31
|
+
initial_max_stream_data_uni: 131072,
|
|
32
|
+
initial_max_streams_bidi: 100,
|
|
33
|
+
initial_max_streams_uni: 3,
|
|
34
|
+
ack_delay_exponent: 3,
|
|
35
|
+
max_ack_delay: 25,
|
|
36
|
+
disable_active_migration: true,
|
|
37
|
+
active_connection_id_limit: 4,
|
|
38
|
+
max_datagram_frame_size: 65527
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function TLSBridge(options) {
|
|
44
|
+
options = options || {};
|
|
45
|
+
|
|
46
|
+
var ev = Emitter();
|
|
47
|
+
var session = null;
|
|
48
|
+
var handshakeSecretsEmitted = false;
|
|
49
|
+
var appSecretsEmitted = false;
|
|
50
|
+
var helloHandled = false;
|
|
51
|
+
var isServer = !!options.isServer;
|
|
52
|
+
var originalDcid = options.originalDcid || new Uint8Array(0);
|
|
53
|
+
var localCid = options.localCid || new Uint8Array(0); // client's own SCID
|
|
54
|
+
var hostname = options.hostname || null;
|
|
55
|
+
// ALPN protocol(s) to advertise. Default ['h3']; normalize string → array.
|
|
56
|
+
var supportedAlpns = (function () {
|
|
57
|
+
var a = options.alpn || ['h3'];
|
|
58
|
+
return Array.isArray(a) ? a : [a];
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
function feedMessage(data) {
|
|
62
|
+
if (!session) {
|
|
63
|
+
var sessionOpts = { isServer: isServer, SNICallback: options.SNICallback };
|
|
64
|
+
if (!isServer) {
|
|
65
|
+
sessionOpts.sessionId = new Uint8Array(0); // QUIC: no compatibility mode
|
|
66
|
+
if (hostname) sessionOpts.servername = hostname;
|
|
67
|
+
}
|
|
68
|
+
session = new TLSSession(sessionOpts);
|
|
69
|
+
setupSessionEvents();
|
|
70
|
+
|
|
71
|
+
// Client: set ALPN + transport params NOW, before setTimeout builds ClientHello
|
|
72
|
+
if (!isServer) {
|
|
73
|
+
var tp = defaultTransportParams();
|
|
74
|
+
tp.initial_source_connection_id = localCid;
|
|
75
|
+
var quicParams = build_transport_params(tp);
|
|
76
|
+
|
|
77
|
+
session.set_context({
|
|
78
|
+
local_supported_versions: [0x0304],
|
|
79
|
+
local_supported_alpns: supportedAlpns,
|
|
80
|
+
local_supported_groups: [29, 23, 24],
|
|
81
|
+
local_supported_cipher_suites: [0x1301, 0x1302],
|
|
82
|
+
local_extensions: [{ type: 0x39, data: quicParams }],
|
|
83
|
+
// Signature schemes offered to the peer (client: in the ClientHello;
|
|
84
|
+
// server: in CertificateRequest). Full modern list, ordered PSS/ECDSA/
|
|
85
|
+
// EdDSA first, legacy pkcs1 last (TLS 1.2 compat only — TLS 1.3 never
|
|
86
|
+
// signs pkcs1). Two bugs lived in the old pkcs1-only list:
|
|
87
|
+
// 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
|
|
88
|
+
// outright (alert 40 handshake_failure — seen live against
|
|
89
|
+
// speed.cloudflare.com; claude.ai worked only because that zone
|
|
90
|
+
// also carries an RSA cert).
|
|
91
|
+
// 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
|
|
92
|
+
// §4.1.2 requires CH2 to match CH1 exactly — offering a different
|
|
93
|
+
// list in CH1 made every HRR flow non-compliant.
|
|
94
|
+
local_supported_signature_algorithms: [
|
|
95
|
+
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
96
|
+
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
|
97
|
+
0x0807, 0x0808, // ed25519, ed448
|
|
98
|
+
0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
|
|
99
|
+
],
|
|
100
|
+
});
|
|
101
|
+
helloHandled = true; // ClientHello will be generated from this set_context
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
session.message(data);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function setupSessionEvents() {
|
|
108
|
+
|
|
109
|
+
// Peer's QUIC transport parameters ride in the TLS quic_transport_parameters
|
|
110
|
+
// extension (type 0x39): client's in the ClientHello, server's in the
|
|
111
|
+
// EncryptedExtensions. lemon-tls surfaces every received handshake message via
|
|
112
|
+
// 'handshakeMessage' with the parsed extensions, so we extract the 0x39 data,
|
|
113
|
+
// parse it, and hand the params to the QUIC layer (which seeds remote_max_data,
|
|
114
|
+
// max_ack_delay, ack_delay_exponent, etc. — previously hardcoded defaults).
|
|
115
|
+
var peerParamsEmitted = false;
|
|
116
|
+
session.on('handshakeMessage', function (type, data, message) {
|
|
117
|
+
if (peerParamsEmitted || !message || !Array.isArray(message.extensions)) return;
|
|
118
|
+
for (var i = 0; i < message.extensions.length; i++) {
|
|
119
|
+
var e = message.extensions[i];
|
|
120
|
+
if (e && (e.type === 0x39 || e.type === 57) && e.data) {
|
|
121
|
+
try {
|
|
122
|
+
var parsed = parse_transport_params(e.data, 0);
|
|
123
|
+
peerParamsEmitted = true;
|
|
124
|
+
if (DEBUG) console.log('[tls] peer transport params parsed (' + e.data.length + 'B)');
|
|
125
|
+
ev.emit('peerTransportParams', parsed);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
if (DEBUG) console.log('[tls] peer transport params parse failed: ' + err.message);
|
|
128
|
+
}
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
session.on('hello', function () {
|
|
135
|
+
if (helloHandled) return; // prevent double-fire
|
|
136
|
+
helloHandled = true;
|
|
137
|
+
if (DEBUG) console.log('[tls] hello — configuring TLS');
|
|
138
|
+
|
|
139
|
+
// Transport params — shared defaults + role-specific fields
|
|
140
|
+
var tp = defaultTransportParams();
|
|
141
|
+
|
|
142
|
+
if (isServer) {
|
|
143
|
+
// Server must echo original DCID and provide SCID
|
|
144
|
+
tp.original_destination_connection_id = originalDcid;
|
|
145
|
+
tp.initial_source_connection_id = originalDcid;
|
|
146
|
+
tp.stateless_reset_token = new Uint8Array(16).fill(0xab); // TODO: random per connection
|
|
147
|
+
} else {
|
|
148
|
+
// Client: initial_source_connection_id = our SCID (not DCID!)
|
|
149
|
+
tp.initial_source_connection_id = localCid;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
var quicParams = build_transport_params(tp);
|
|
153
|
+
|
|
154
|
+
var tlsContext = {
|
|
155
|
+
local_supported_versions: [0x0304],
|
|
156
|
+
local_supported_alpns: supportedAlpns,
|
|
157
|
+
local_supported_groups: [29, 23, 24],
|
|
158
|
+
local_supported_cipher_suites: [0x1301, 0x1302, 0xc02f, 0xc030, 0xcca8],
|
|
159
|
+
local_extensions: [
|
|
160
|
+
{ type: 0x39, data: quicParams }
|
|
161
|
+
],
|
|
162
|
+
// Signature schemes offered to the peer (client: in the ClientHello;
|
|
163
|
+
// server: in CertificateRequest). Full modern list, ordered PSS/ECDSA/
|
|
164
|
+
// EdDSA first, legacy pkcs1 last (TLS 1.2 compat only — TLS 1.3 never
|
|
165
|
+
// signs pkcs1). Two bugs lived in the old pkcs1-only list:
|
|
166
|
+
// 1. Cloudflare zones with ECDSA-only certs reject the ClientHello
|
|
167
|
+
// outright (alert 40 handshake_failure — seen live against
|
|
168
|
+
// speed.cloudflare.com; claude.ai worked only because that zone
|
|
169
|
+
// also carries an RSA cert).
|
|
170
|
+
// 2. lemon-tls hardcodes THIS full list in the HRR CH2, and RFC 8446
|
|
171
|
+
// §4.1.2 requires CH2 to match CH1 exactly — offering a different
|
|
172
|
+
// list in CH1 made every HRR flow non-compliant.
|
|
173
|
+
local_supported_signature_algorithms: [
|
|
174
|
+
0x0804, 0x0805, 0x0806, // rsa_pss_rsae_sha256/384/512
|
|
175
|
+
0x0403, 0x0503, 0x0603, // ecdsa_secp256r1/384r1/521r1
|
|
176
|
+
0x0807, 0x0808, // ed25519, ed448
|
|
177
|
+
0x0401, 0x0501, 0x0601 // rsa_pkcs1 (TLS 1.2 legacy)
|
|
178
|
+
],
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
session.set_context(tlsContext);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
session.on('message', function (epoch, seq, type, data) {
|
|
185
|
+
var quicEpoch;
|
|
186
|
+
if (epoch === 0) quicEpoch = 'initial';
|
|
187
|
+
else if (epoch === 1) quicEpoch = 'handshake';
|
|
188
|
+
else quicEpoch = 'app';
|
|
189
|
+
|
|
190
|
+
if (DEBUG) console.log('[tls] outgoing: epoch=' + quicEpoch + ' type=' + type + ' len=' + data.length);
|
|
191
|
+
ev.emit('send', quicEpoch, data);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
// Native events from LemonTLS — no polling needed
|
|
195
|
+
session.on('handshakeSecrets', function (localSecret, remoteSecret) {
|
|
196
|
+
if (handshakeSecretsEmitted) return;
|
|
197
|
+
handshakeSecretsEmitted = true;
|
|
198
|
+
var cipher = session.getCipher();
|
|
199
|
+
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
200
|
+
if (DEBUG) console.log('[tls] handshake secrets ready');
|
|
201
|
+
ev.emit('handshakeSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
session.on('appSecrets', function (localSecret, remoteSecret) {
|
|
205
|
+
if (appSecretsEmitted) return;
|
|
206
|
+
appSecretsEmitted = true;
|
|
207
|
+
var cipher = session.getCipher();
|
|
208
|
+
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
209
|
+
if (DEBUG) console.log('[tls] app secrets ready');
|
|
210
|
+
ev.emit('appSecrets', { local: localSecret, remote: remoteSecret, cipher: cipher, hash: hashName });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
session.on('secureConnect', function () {
|
|
214
|
+
if (DEBUG) console.log('[tls] secureConnect');
|
|
215
|
+
ev.emit('secureConnect');
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
session.on('keyUpdate', function (info) {
|
|
219
|
+
ev.emit('keyUpdate', info.direction, info.secret);
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
on: function (name, fn) { ev.on(name, fn); },
|
|
225
|
+
off: function (name, fn) { ev.off(name, fn); },
|
|
226
|
+
feedMessage: feedMessage,
|
|
227
|
+
requestKeyUpdate: function () { if (session) session.requestKeyUpdate(true); },
|
|
228
|
+
getHandshakeSecrets: function () {
|
|
229
|
+
if (!session) return null;
|
|
230
|
+
var hs = session.getHandshakeSecrets();
|
|
231
|
+
if (!hs || !hs.localSecret) return null;
|
|
232
|
+
var cipher = session.getCipher();
|
|
233
|
+
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
234
|
+
return { local: hs.localSecret, remote: hs.remoteSecret, cipher: cipher, hash: hashName };
|
|
235
|
+
},
|
|
236
|
+
getTrafficSecrets: function () {
|
|
237
|
+
if (!session) return null;
|
|
238
|
+
var secrets = session.getTrafficSecrets();
|
|
239
|
+
if (!secrets || !secrets.localAppSecret) return null;
|
|
240
|
+
var cipher = session.getCipher();
|
|
241
|
+
var hashName = TLS_CIPHER_SUITES[cipher] ? TLS_CIPHER_SUITES[cipher].hash : 'sha256';
|
|
242
|
+
return { local: secrets.localAppSecret, remote: secrets.remoteAppSecret, cipher: cipher, hash: hashName };
|
|
243
|
+
},
|
|
244
|
+
getCipher: function () { return session ? session.getCipher() : null; },
|
|
245
|
+
close: function () { if (session && session.close) session.close(); }
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Drop-in replacement for tls.createSecureContext.
|
|
251
|
+
* Builds both a lemon-tls context (for H3/QUIC) and a node:tls
|
|
252
|
+
* context (for HTTP/2 + HTTP/1.1 over TCP) from the same credentials.
|
|
253
|
+
*/
|
|
254
|
+
function createSecureContext(options) {
|
|
255
|
+
var ctx = _lemonCreateSecureContext(options);
|
|
256
|
+
if (ctx && options) {
|
|
257
|
+
ctx._rawKey = options.key || null;
|
|
258
|
+
ctx._rawCert = options.cert || null;
|
|
259
|
+
ctx._rawCa = options.ca || null;
|
|
260
|
+
try {
|
|
261
|
+
var nodeOpts = {};
|
|
262
|
+
if (options.key) nodeOpts.key = options.key;
|
|
263
|
+
if (options.cert) nodeOpts.cert = options.cert;
|
|
264
|
+
if (options.ca) nodeOpts.ca = options.ca;
|
|
265
|
+
ctx._nodeContext = nodeTls.createSecureContext(nodeOpts);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
ctx._nodeContext = null;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return ctx;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
export { TLSBridge, createSecureContext };
|