quico 0.4.0 → 0.7.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 +1768 -155
- package/index.js +55 -36
- package/package.json +3 -3
- 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 +770 -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 +624 -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 +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +467 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6517 -657
- package/src/quic_server.js +768 -73
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +361 -35
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +474 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
package/src/quic_socket.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
|
|
15
15
|
import dgram from 'node:dgram';
|
|
16
16
|
import { QUICConnection } from './quic_connection.js';
|
|
17
|
+
import lemonTls from 'lemon-tls';
|
|
18
|
+
import { SUPPORTED_VERSIONS, parseVersionList, versionName } from './quic_version.js';
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -33,6 +35,16 @@ import { QUICConnection } from './quic_connection.js';
|
|
|
33
35
|
* ['doq'] for DNS-over-QUIC).
|
|
34
36
|
* rejectUnauthorized — verify the server certificate (default true)
|
|
35
37
|
* ca — CA certificate(s) for verification
|
|
38
|
+
* idleTimeout, keepAlive — as QUICConnection's (the idle timeout in force
|
|
39
|
+
* is the smaller of the two sides', RFC 9000 §10.1)
|
|
40
|
+
* key, cert, passphrase — a client certificate, sent if the server asks
|
|
41
|
+
* for one (requestCert), as with tls.connect
|
|
42
|
+
* quicVersions — QUIC versions, the first one's first flight
|
|
43
|
+
* and the rest offered (default ['v1', 'v2'])
|
|
44
|
+
* token — a token from an earlier connection's
|
|
45
|
+
* 'new_token' event (NEW_TOKEN), used once
|
|
46
|
+
* session — a session saved from an earlier connection's
|
|
47
|
+
* 'session' event, to resume (tls.connect's name)
|
|
36
48
|
* onSocket(quic, socket) — called as soon as the QUICConnection exists,
|
|
37
49
|
* BEFORE the handshake starts. Use this to attach
|
|
38
50
|
* listeners that must observe the handshake, such
|
|
@@ -46,6 +58,14 @@ import { QUICConnection } from './quic_connection.js';
|
|
|
46
58
|
* socket binds, so it is delivered via onConnect rather than returned here.
|
|
47
59
|
*/
|
|
48
60
|
function createQuicClientSocket(opts) {
|
|
61
|
+
// ecdhCurve (tls.connect's): the key-exchange groups to offer, in order —
|
|
62
|
+
// 'X25519MLKEM768:X25519' for the post-quantum hybrid first. Unset or
|
|
63
|
+
// 'auto': X25519, P-256, P-384. A name not implemented (or the hybrid in a
|
|
64
|
+
// Node without ML-KEM) throws here, as tls.connect's does.
|
|
65
|
+
var groups = lemonTls.parseEcdhCurve(opts.ecdhCurve);
|
|
66
|
+
// sigalgs (tls.connect's): the signature schemes to offer, in order; unset,
|
|
67
|
+
// lemon-tls's list. A name it does not know throws here.
|
|
68
|
+
var sigalgs = lemonTls.parseSigalgs(opts.sigalgs);
|
|
49
69
|
var isIPv6 = opts.remoteIp.indexOf(':') >= 0;
|
|
50
70
|
var udpSocket = dgram.createSocket(isIPv6 ? 'udp6' : 'udp4');
|
|
51
71
|
var quic = null;
|
|
@@ -74,7 +94,13 @@ function createQuicClientSocket(opts) {
|
|
|
74
94
|
try { udpSocket.setSendBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
75
95
|
try { udpSocket.setRecvBufferSize(2 * 1024 * 1024); } catch (e) {}
|
|
76
96
|
|
|
77
|
-
|
|
97
|
+
// One attempt; after a Version Negotiation, a second in a version the
|
|
98
|
+
// server offered (RFC 9000 §6.2) — once.
|
|
99
|
+
start(null, null);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function start(version, vnOffered) {
|
|
103
|
+
var q = quic = new QUICConnection({
|
|
78
104
|
isServer: false,
|
|
79
105
|
hostname: opts.hostname,
|
|
80
106
|
// Without this the client always offered 'h3' regardless of what the
|
|
@@ -88,7 +114,45 @@ function createQuicClientSocket(opts) {
|
|
|
88
114
|
// Defaults to true. Pass false for self-signed certificates, or supply
|
|
89
115
|
// `ca` for a private CA.
|
|
90
116
|
rejectUnauthorized: opts.rejectUnauthorized !== false,
|
|
91
|
-
|
|
117
|
+
// A client certificate, for a server that asks (tls.connect's names).
|
|
118
|
+
key: opts.key || null, cert: opts.cert || null, passphrase: opts.passphrase,
|
|
119
|
+
ca: opts.ca || null,
|
|
120
|
+
session: opts.session || null,
|
|
121
|
+
requestOCSP: opts.requestOCSP === true,
|
|
122
|
+
// Encrypted Client Hello: the server's ECHConfigList, or GREASE ECH.
|
|
123
|
+
ech: opts.ech || null,
|
|
124
|
+
echGrease: opts.echGrease === true,
|
|
125
|
+
// The groups to offer (ecdhCurve, above); null: the default.
|
|
126
|
+
groups: groups,
|
|
127
|
+
sigalgs: sigalgs,
|
|
128
|
+
// A NEW_TOKEN token from an earlier connection to the server (RFC 9000
|
|
129
|
+
// §8.1.3), for the Initial packets.
|
|
130
|
+
token: opts.token || null,
|
|
131
|
+
// quic.remoteAddress / remotePort, as on a net.Socket.
|
|
132
|
+
remoteAddress: opts.remoteIp,
|
|
133
|
+
remotePort: opts.remotePort,
|
|
134
|
+
// Our transport parameters, where the caller set them (tls_bridge).
|
|
135
|
+
transportParams: opts.transportParams || null,
|
|
136
|
+
// The connection's own options: idleTimeout (ms; or
|
|
137
|
+
// transportParams.maxIdleTimeout) and keepAlive (true: PING at half the
|
|
138
|
+
// idle timeout in force; a number: every that many ms).
|
|
139
|
+
idleTimeout: opts.idleTimeout,
|
|
140
|
+
keepAlive: opts.keepAlive,
|
|
141
|
+
// The congestion window it starts with, where the caller set it.
|
|
142
|
+
initialCongestionWindow: opts.initialCongestionWindow,
|
|
143
|
+
// The connection's options (quic_connection's CONNECTION_OPTIONS).
|
|
144
|
+
startupCap: opts.startupCap,
|
|
145
|
+
pmtuDiscovery: opts.pmtuDiscovery,
|
|
146
|
+
maxUdpPayloadSize: opts.maxUdpPayloadSize,
|
|
147
|
+
initialRtt: opts.initialRtt,
|
|
148
|
+
reorderWindow: opts.reorderWindow,
|
|
149
|
+
// 0-RTT with `session` when its ticket allows it (RFC 9001 §4.6).
|
|
150
|
+
earlyData: opts.earlyData === true,
|
|
151
|
+
// The QUIC versions: the first flight's, then the others accepted
|
|
152
|
+
// (offered through version_information, RFC 9368). ['v1', 'v2'].
|
|
153
|
+
quicVersions: opts.quicVersions,
|
|
154
|
+
version: version || undefined,
|
|
155
|
+
versionNegotiation: vnOffered || undefined
|
|
92
156
|
});
|
|
93
157
|
|
|
94
158
|
// Hand the connection to the caller BEFORE connect() runs.
|
|
@@ -100,25 +164,86 @@ function createQuicClientSocket(opts) {
|
|
|
100
164
|
// in onConnect receives nothing at all, and the client half of a capture
|
|
101
165
|
// is undecryptable. onSocket gives callers a hook at the only moment that
|
|
102
166
|
// works.
|
|
103
|
-
if (opts.onSocket) opts.onSocket(
|
|
167
|
+
if (opts.onSocket) opts.onSocket(q, udpSocket);
|
|
104
168
|
|
|
105
|
-
|
|
169
|
+
q.on('packet', function (data) {
|
|
106
170
|
udpSocket.send(data, opts.remotePort, opts.remoteIp, function (err) {
|
|
107
171
|
if (err && opts.onError) opts.onError(err);
|
|
108
172
|
});
|
|
109
173
|
});
|
|
110
174
|
|
|
111
|
-
|
|
112
|
-
|
|
175
|
+
// A connection that never connects is a failure the caller must hear
|
|
176
|
+
// about, once: the TLS reason when there is one (a refused certificate,
|
|
177
|
+
// with node:tls's code), the handshake timeout, or — when the peer simply
|
|
178
|
+
// closed — its CONNECTION_CLOSE. Before this a refused certificate closed
|
|
179
|
+
// the connection in silence, and an HTTP/3 request or a WebTransport
|
|
180
|
+
// `ready` waited on it forever.
|
|
181
|
+
var connected = false, failed = false;
|
|
182
|
+
function fail(err) {
|
|
183
|
+
if (connected || failed) return;
|
|
184
|
+
failed = true;
|
|
185
|
+
if (opts.onError) opts.onError(err);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
q.on('error', function (err) { if (q === quic) fail(err); });
|
|
189
|
+
|
|
190
|
+
q.on('connect', function () {
|
|
191
|
+
connected = true;
|
|
192
|
+
if (opts.onConnect) opts.onConnect(q, udpSocket);
|
|
113
193
|
});
|
|
114
194
|
|
|
115
|
-
|
|
195
|
+
// 0-RTT: the connection may carry application data now, before the
|
|
196
|
+
// handshake — onEarly(quic, socket). Refused by the server, what went is
|
|
197
|
+
// forgotten and 'early_rejected' says so (send it again after onConnect).
|
|
198
|
+
q.on('early', function () { if (opts.onEarly) opts.onEarly(q, udpSocket); });
|
|
199
|
+
|
|
200
|
+
// The server speaks none of the versions offered in this attempt: a new
|
|
201
|
+
// one, in the first of ours it offers. A Version Negotiation that
|
|
202
|
+
// answers the second attempt is not followed (§6.2: once).
|
|
203
|
+
q.on('version_negotiation', function (offered) {
|
|
204
|
+
if (vnOffered) return;
|
|
205
|
+
var prefs = opts.quicVersions ? parseVersionList(opts.quicVersions, 'quicVersions') : SUPPORTED_VERSIONS;
|
|
206
|
+
var pick = null;
|
|
207
|
+
for (var i = 0; i < prefs.length && pick === null; i++) if (offered.indexOf(prefs[i]) >= 0) pick = prefs[i];
|
|
208
|
+
if (pick === null) {
|
|
209
|
+
var err = new Error('the server supports none of our QUIC versions (it offers ' + offered.map(versionName).join(', ') + ')');
|
|
210
|
+
err.code = 'ERR_QUIC_VERSION_NEGOTIATION';
|
|
211
|
+
fail(err);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
start(pick, offered);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
function closedError() {
|
|
218
|
+
var pc = quic.context && quic.context.peer_close;
|
|
219
|
+
var lc = !pc && quic.context && quic.context.close_frame;
|
|
220
|
+
var err = new Error('QUIC connection closed before the handshake completed' +
|
|
221
|
+
(pc ? ' (peer error 0x' + pc.error.toString(16) + (pc.reason ? ': ' + pc.reason : '') + ')' :
|
|
222
|
+
lc ? ' (error 0x' + lc.error.toString(16) + (lc.reason ? ': ' + lc.reason : '') + ')' : ''));
|
|
223
|
+
err.code = 'ERR_QUIC_HANDSHAKE_FAILED';
|
|
224
|
+
return err;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// A handshake that will not complete is reported when that is known —
|
|
228
|
+
// the peer's CONNECTION_CLOSE, or ours — not when the connection's drain
|
|
229
|
+
// (RFC 9000 §10.2, up to 3 s) ends and 'close' comes: a server that
|
|
230
|
+
// refused the ClientHello was heard 3 s late, and an HTTP/3 request
|
|
231
|
+
// waited that long to fail. On the next turn: a TLS alert of ours closes
|
|
232
|
+
// the connection first and gives its reason ('error') right after.
|
|
233
|
+
q.on('handshake_ended', function (confirmed) {
|
|
234
|
+
if (confirmed || q !== quic) return;
|
|
235
|
+
setImmediate(function () { if (q === quic && !connected) fail(closedError()); });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
q.on('close', function () {
|
|
239
|
+
if (q !== quic) return; // an attempt given up for another
|
|
116
240
|
try { udpSocket.close(); } catch (e) {}
|
|
241
|
+
if (!connected) fail(closedError());
|
|
117
242
|
if (opts.onClose) opts.onClose();
|
|
118
243
|
});
|
|
119
244
|
|
|
120
|
-
|
|
121
|
-
}
|
|
245
|
+
q.connect();
|
|
246
|
+
}
|
|
122
247
|
|
|
123
248
|
return udpSocket;
|
|
124
249
|
}
|
|
@@ -0,0 +1,143 @@
|
|
|
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
|
+
|
|
16
|
+
// ============================================================
|
|
17
|
+
// QUIC versions — what differs between them, in one place
|
|
18
|
+
//
|
|
19
|
+
// QUIC v1 (RFC 9000/9001) and v2 (RFC 9369) are one protocol: the same
|
|
20
|
+
// frames, the same handshake, the same recovery. v2 changes what an
|
|
21
|
+
// on-path device could have learned to depend on — the version number, the
|
|
22
|
+
// salt the Initial keys come from, the labels every packet key is derived
|
|
23
|
+
// with, the Retry integrity key, and which long-header type bits mean
|
|
24
|
+
// Initial, 0-RTT, Handshake and Retry — so that QUIC stays free to change
|
|
25
|
+
// (anti-ossification). Everything a version decides lives in its profile
|
|
26
|
+
// here, and every place that builds or reads a packet, or derives a key,
|
|
27
|
+
// asks the connection's profile: the rest of the code is the same for all
|
|
28
|
+
// versions, as lemon-tls's is for TLS 1.2 and 1.3.
|
|
29
|
+
//
|
|
30
|
+
// A new version is a profile here, and its entry in COMPATIBLE when a first
|
|
31
|
+
// flight in one can be read as the other (RFC 9368 §2.3).
|
|
32
|
+
// ============================================================
|
|
33
|
+
|
|
34
|
+
var QUIC_V1 = 0x00000001;
|
|
35
|
+
var QUIC_V2 = 0x6b3343cf;
|
|
36
|
+
|
|
37
|
+
function hex(s) { return new Uint8Array(Buffer.from(s, 'hex')); }
|
|
38
|
+
|
|
39
|
+
var PROFILES = {};
|
|
40
|
+
|
|
41
|
+
PROFILES[QUIC_V1] = {
|
|
42
|
+
version: QUIC_V1,
|
|
43
|
+
name: 'v1',
|
|
44
|
+
// RFC 9001 §5.2
|
|
45
|
+
initialSalt: hex('38762cf7f55934b34d179ae6a4c80cadccbb7f0a'),
|
|
46
|
+
// RFC 9001 §5.1, §6.1
|
|
47
|
+
labels: { key: 'quic key', iv: 'quic iv', hp: 'quic hp', ku: 'quic ku' },
|
|
48
|
+
// RFC 9001 §5.8
|
|
49
|
+
retryKey: hex('be0c690b9f66575a1d766b54e368c84e'),
|
|
50
|
+
retryNonce: hex('461599d35d632bf2239825bb'),
|
|
51
|
+
// RFC 9000 §17.2: long header type bits → packet type
|
|
52
|
+
longTypes: ['initial', '0rtt', 'handshake', 'retry']
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
PROFILES[QUIC_V2] = {
|
|
56
|
+
version: QUIC_V2,
|
|
57
|
+
name: 'v2',
|
|
58
|
+
// RFC 9369 §3.3.1
|
|
59
|
+
initialSalt: hex('0dede3def700a6db819381be6e269dcbf9bd2ed9'),
|
|
60
|
+
// RFC 9369 §3.3.2
|
|
61
|
+
labels: { key: 'quicv2 key', iv: 'quicv2 iv', hp: 'quicv2 hp', ku: 'quicv2 ku' },
|
|
62
|
+
// RFC 9369 §3.3.3
|
|
63
|
+
retryKey: hex('8fb4b01b56ac48e260fbcbcead7ccc92'),
|
|
64
|
+
retryNonce: hex('d86969bc2d7c6d9990efb04a'),
|
|
65
|
+
// RFC 9369 §3.2: Retry 0b00, Initial 0b01, 0-RTT 0b10, Handshake 0b11
|
|
66
|
+
longTypes: ['retry', 'initial', '0rtt', 'handshake']
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Which first flights can be read as another version's (RFC 9368 §2.3):
|
|
70
|
+
// v1 and v2 differ in nothing a ClientHello carries, both ways (RFC 9369 §4).
|
|
71
|
+
var COMPATIBLE = {};
|
|
72
|
+
COMPATIBLE[QUIC_V1] = [QUIC_V2];
|
|
73
|
+
COMPATIBLE[QUIC_V2] = [QUIC_V1];
|
|
74
|
+
|
|
75
|
+
/** The versions this implementation speaks, in its default preference. */
|
|
76
|
+
var SUPPORTED_VERSIONS = [QUIC_V1, QUIC_V2];
|
|
77
|
+
|
|
78
|
+
function versionProfile(version) {
|
|
79
|
+
return PROFILES[version >>> 0] || null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isSupportedVersion(version) {
|
|
83
|
+
return !!PROFILES[version >>> 0];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Whether a first flight in `from` can be taken as one in `to`. */
|
|
87
|
+
function isCompatible(from, to) {
|
|
88
|
+
from >>>= 0; to >>>= 0;
|
|
89
|
+
return from === to || (COMPATIBLE[from] || []).indexOf(to) >= 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Long header type bits → 'initial' | '0rtt' | 'handshake' | 'retry', or null. */
|
|
93
|
+
function longPacketType(version, typeBits) {
|
|
94
|
+
var p = PROFILES[version >>> 0];
|
|
95
|
+
return p ? p.longTypes[typeBits & 3] : null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Packet type → its long header type bits in `version`. */
|
|
99
|
+
function longTypeBits(version, packetType) {
|
|
100
|
+
var p = PROFILES[version >>> 0];
|
|
101
|
+
var i = p ? p.longTypes.indexOf(packetType) : -1;
|
|
102
|
+
if (i < 0) throw new Error('no long header type ' + packetType + ' in QUIC version 0x' + (version >>> 0).toString(16));
|
|
103
|
+
return i;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* A version as an application names it — 1, 0x6b3343cf, 'v1', 'v2' — as a
|
|
108
|
+
* version number; throws on one this implementation does not speak.
|
|
109
|
+
*/
|
|
110
|
+
function parseVersion(v, what) {
|
|
111
|
+
var n = null;
|
|
112
|
+
if (typeof v === 'number' && Number.isInteger(v)) n = v >>> 0;
|
|
113
|
+
else if (typeof v === 'string') {
|
|
114
|
+
var s = v.toLowerCase();
|
|
115
|
+
for (var k in PROFILES) if (PROFILES[k].name === s) n = PROFILES[k].version;
|
|
116
|
+
}
|
|
117
|
+
if (n === null || !PROFILES[n]) throw new TypeError((what || 'quicVersions') + ': unsupported QUIC version ' + JSON.stringify(v) + ' (v1 or v2)');
|
|
118
|
+
return n;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** An option listing versions in preference order → [number, …]. */
|
|
122
|
+
function parseVersionList(list, what) {
|
|
123
|
+
if (list === undefined || list === null) return null;
|
|
124
|
+
if (!Array.isArray(list) || list.length === 0) throw new TypeError((what || 'quicVersions') + ' must be a non-empty array of QUIC versions');
|
|
125
|
+
var out = [];
|
|
126
|
+
for (var i = 0; i < list.length; i++) {
|
|
127
|
+
var n = parseVersion(list[i], what);
|
|
128
|
+
if (out.indexOf(n) < 0) out.push(n);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function versionName(version) {
|
|
134
|
+
var p = PROFILES[version >>> 0];
|
|
135
|
+
return p ? p.name : '0x' + (version >>> 0).toString(16);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export {
|
|
139
|
+
QUIC_V1, QUIC_V2, SUPPORTED_VERSIONS,
|
|
140
|
+
versionProfile, isSupportedVersion, isCompatible,
|
|
141
|
+
longPacketType, longTypeBits,
|
|
142
|
+
parseVersion, parseVersionList, versionName
|
|
143
|
+
};
|