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,314 @@
|
|
|
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 dgram from 'node:dgram';
|
|
16
|
+
import { DEBUG, Emitter } from './utils.js';
|
|
17
|
+
import { parse_quic_datagram } from './transport.js';
|
|
18
|
+
import { QUICConnection } from './quic_connection.js';
|
|
19
|
+
import { createSecureContext } from './tls_bridge.js';
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
// ============================================================
|
|
23
|
+
// createQuicServer(options)
|
|
24
|
+
//
|
|
25
|
+
// Transport-only QUIC server. Owns the UDP socket(s), demultiplexes
|
|
26
|
+
// incoming datagrams to per-peer QUICConnection objects, and emits a
|
|
27
|
+
// 'connection' event carrying the raw QUICConnection. It knows nothing
|
|
28
|
+
// about HTTP/3 — application protocols (HTTP/3, DNS-over-QUIC, ...) are
|
|
29
|
+
// layered on top by listening for 'connection' and then using the
|
|
30
|
+
// connection's own stream/datagram API.
|
|
31
|
+
//
|
|
32
|
+
// This is the connection-management harness that used to live inside
|
|
33
|
+
// h3_server.js. h3_server now builds on it (wrapping each connection in
|
|
34
|
+
// an H3Connection); other consumers (e.g. a DoQ server) can use it
|
|
35
|
+
// directly.
|
|
36
|
+
//
|
|
37
|
+
// Events:
|
|
38
|
+
// 'connection' (quic, peer) — new QUICConnection created for a peer.
|
|
39
|
+
// peer = { address, port }. Handlers
|
|
40
|
+
// attach quic.on('connect'|'stream'|
|
|
41
|
+
// 'datagram'|'close') as needed.
|
|
42
|
+
// 'error' (err) — UDP socket / send error.
|
|
43
|
+
//
|
|
44
|
+
// Options:
|
|
45
|
+
// alpn — ALPN protocol(s) to advertise (default 'h3').
|
|
46
|
+
// DoQ servers pass 'doq'.
|
|
47
|
+
// SNICallback — TLS SNI callback returning a SecureContext.
|
|
48
|
+
// key/cert/ca — convenience: build a default SecureContext when no
|
|
49
|
+
// SNICallback is given.
|
|
50
|
+
// maxConnections — capacity cap (default 10000).
|
|
51
|
+
// socket/socket6 — externally-owned, already-bound UDP sockets
|
|
52
|
+
// (shared-port mode, RFC 9443). When provided, the
|
|
53
|
+
// caller feeds datagrams via handlePacket(msg, rinfo).
|
|
54
|
+
// ============================================================
|
|
55
|
+
|
|
56
|
+
function createQuicServer(options) {
|
|
57
|
+
options = options || {};
|
|
58
|
+
|
|
59
|
+
var ev = Emitter();
|
|
60
|
+
var udp4 = null;
|
|
61
|
+
var udp6 = null;
|
|
62
|
+
var port = null;
|
|
63
|
+
|
|
64
|
+
// ALPN to advertise on every accepted connection. Default ['h3'].
|
|
65
|
+
var alpn = options.alpn || ['h3'];
|
|
66
|
+
|
|
67
|
+
// ── External socket mode (shared UDP port) ──
|
|
68
|
+
// When socket/socket6 are provided, the UDP layer is owned externally.
|
|
69
|
+
// The caller demuxes packets and feeds them via handlePacket(msg, rinfo).
|
|
70
|
+
// See RFC 9443 for the shared-port multiplexing scheme.
|
|
71
|
+
var externalSocket4 = options.socket || null;
|
|
72
|
+
var externalSocket6 = options.socket6 || null;
|
|
73
|
+
|
|
74
|
+
if (externalSocket4) {
|
|
75
|
+
try { externalSocket4.address(); }
|
|
76
|
+
catch (e) { throw new Error('options.socket must be bound before createQuicServer()'); }
|
|
77
|
+
}
|
|
78
|
+
if (externalSocket6) {
|
|
79
|
+
try { externalSocket6.address(); }
|
|
80
|
+
catch (e) { throw new Error('options.socket6 must be bound before createQuicServer()'); }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
var isExternal = !!(externalSocket4 || externalSocket6);
|
|
84
|
+
|
|
85
|
+
// Build SNICallback
|
|
86
|
+
var sniCallback = options.SNICallback || null;
|
|
87
|
+
|
|
88
|
+
if (!sniCallback && options.key && options.cert) {
|
|
89
|
+
var defaultCtx = createSecureContext({ key: options.key, cert: options.cert, ca: options.ca });
|
|
90
|
+
sniCallback = function (servername, cb) { cb(null, defaultCtx); };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ---- Connection tracking ----
|
|
94
|
+
var connections = {}; // connId → { quic, peer }
|
|
95
|
+
var addressMap = {}; // "ip:port" → connId
|
|
96
|
+
var maxConnections = options.maxConnections || 10000;
|
|
97
|
+
var sweepTimer = null;
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// ============================================================
|
|
101
|
+
// UDP → connection routing
|
|
102
|
+
// ============================================================
|
|
103
|
+
|
|
104
|
+
function onUdpMessage(msg, rinfo) {
|
|
105
|
+
var data = new Uint8Array(msg);
|
|
106
|
+
var fromIp = rinfo.address;
|
|
107
|
+
var fromPort = rinfo.port;
|
|
108
|
+
if (DEBUG) console.log('[qserver] UDP from ' + fromIp + ':' + fromPort + ' len=' + data.length);
|
|
109
|
+
|
|
110
|
+
var addressKey = fromIp + ':' + fromPort;
|
|
111
|
+
var packets = parse_quic_datagram(data);
|
|
112
|
+
if (packets.length === 0) return;
|
|
113
|
+
|
|
114
|
+
// Route the whole datagram by its first packet. Coalesced packets in one
|
|
115
|
+
// UDP datagram belong to the same connection (RFC 9000 §12.2), so a single
|
|
116
|
+
// routing decision covers them all — and the datagram is parsed only once
|
|
117
|
+
// here, handing the parsed packets straight to the connection (previously
|
|
118
|
+
// each packet's raw bytes were re-parsed inside feedDatagram).
|
|
119
|
+
var connId = findConnectionId(packets[0], addressKey);
|
|
120
|
+
if (connId === null) return; // stray/unmatched non-Initial packet — drop
|
|
121
|
+
|
|
122
|
+
if (!(connId in connections)) {
|
|
123
|
+
// Reject new connections when at capacity
|
|
124
|
+
if (Object.keys(connections).length >= maxConnections) return;
|
|
125
|
+
createConnection(connId, fromIp, fromPort, addressKey);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
connections[connId].quic.feedPackets(fromIp, fromPort, packets);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
function findConnectionId(pkt, addressKey) {
|
|
133
|
+
var dcidStr = null;
|
|
134
|
+
if (pkt.dcid && pkt.dcid.byteLength > 0) {
|
|
135
|
+
dcidStr = Array.from(pkt.dcid).join('');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (dcidStr && dcidStr in connections) return dcidStr;
|
|
139
|
+
if (addressKey in addressMap && addressMap[addressKey] in connections) return addressMap[addressKey];
|
|
140
|
+
|
|
141
|
+
// No existing connection matches. Only an Initial packet may *start* a new
|
|
142
|
+
// connection; any other unmatched packet (a stray/late short-header packet,
|
|
143
|
+
// or a Handshake whose Initial was lost, etc.) is dropped rather than
|
|
144
|
+
// spawning a phantom connection keyed by a random id.
|
|
145
|
+
if (pkt.type !== 'initial') return null;
|
|
146
|
+
|
|
147
|
+
var id = dcidStr || String(Math.floor(Math.random() * 9007199254740991));
|
|
148
|
+
addressMap[addressKey] = id;
|
|
149
|
+
return id;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
function createConnection(connId, fromIp, fromPort, addressKey) {
|
|
154
|
+
var quic = new QUICConnection({ SNICallback: sniCallback, alpn: alpn });
|
|
155
|
+
|
|
156
|
+
connections[connId] = {
|
|
157
|
+
quic: quic,
|
|
158
|
+
peer: { address: fromIp, port: fromPort }
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
addressMap[addressKey] = connId;
|
|
162
|
+
|
|
163
|
+
// Send packets back via UDP
|
|
164
|
+
quic.on('packet', function (data) {
|
|
165
|
+
sendUdp(fromIp, fromPort, data);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Drop our bookkeeping when the connection closes.
|
|
169
|
+
quic.on('close', function () {
|
|
170
|
+
delete connections[connId];
|
|
171
|
+
if (addressMap[addressKey] === connId) delete addressMap[addressKey];
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Hand the raw connection to the application layer (H3, DoQ, ...).
|
|
175
|
+
ev.emit('connection', quic, { address: fromIp, port: fromPort });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// ============================================================
|
|
180
|
+
// UDP send
|
|
181
|
+
// ============================================================
|
|
182
|
+
|
|
183
|
+
function sendUdp(toIp, toPort, data) {
|
|
184
|
+
var isV6 = toIp.indexOf(':') >= 0;
|
|
185
|
+
// Prefer external socket (shared mode) over internally-bound socket
|
|
186
|
+
var socket = isV6
|
|
187
|
+
? (externalSocket6 || udp6)
|
|
188
|
+
: (externalSocket4 || udp4);
|
|
189
|
+
if (!socket) return;
|
|
190
|
+
socket.send(data, toPort, toIp, function (err) {
|
|
191
|
+
if (err) ev.emit('error', err);
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// ============================================================
|
|
197
|
+
// listen / close
|
|
198
|
+
// ============================================================
|
|
199
|
+
|
|
200
|
+
function listen(listenPort, host, callback) {
|
|
201
|
+
if (typeof host === 'function') { callback = host; host = null; }
|
|
202
|
+
port = listenPort || 443;
|
|
203
|
+
host = host || '::';
|
|
204
|
+
|
|
205
|
+
if (isExternal) {
|
|
206
|
+
// External sockets already bound — wire up incoming messages.
|
|
207
|
+
// Starting the sweep timer is safe here (idempotent check below).
|
|
208
|
+
if (externalSocket4) {
|
|
209
|
+
externalSocket4.on('message', onUdpMessage);
|
|
210
|
+
}
|
|
211
|
+
if (externalSocket6) {
|
|
212
|
+
externalSocket6.on('message', onUdpMessage);
|
|
213
|
+
}
|
|
214
|
+
startSweepTimer();
|
|
215
|
+
if (typeof callback === 'function') setImmediate(callback);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
udp4 = dgram.createSocket('udp4');
|
|
220
|
+
udp4.on('message', onUdpMessage);
|
|
221
|
+
udp4.on('error', function (err) { ev.emit('error', err); });
|
|
222
|
+
var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
|
|
223
|
+
udp4.bind(port, host4);
|
|
224
|
+
|
|
225
|
+
udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
|
|
226
|
+
udp6.on('message', onUdpMessage);
|
|
227
|
+
udp6.on('error', function (err) { ev.emit('error', err); });
|
|
228
|
+
var host6 = host.indexOf(':') !== -1 ? host : '::';
|
|
229
|
+
udp6.bind(port, host6, function () {
|
|
230
|
+
if (typeof callback === 'function') callback();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
startSweepTimer();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Periodic sweep for dead connections (safety net). Runs independently
|
|
237
|
+
// of socket ownership so external-mode agents get the same cleanup.
|
|
238
|
+
function startSweepTimer() {
|
|
239
|
+
if (sweepTimer) return; // idempotent
|
|
240
|
+
sweepTimer = setInterval(function () {
|
|
241
|
+
for (var id in connections) {
|
|
242
|
+
var st = connections[id].quic.state;
|
|
243
|
+
if (st === 'closed') {
|
|
244
|
+
delete connections[id];
|
|
245
|
+
// Clean address map
|
|
246
|
+
for (var addr in addressMap) {
|
|
247
|
+
if (addressMap[addr] === id) delete addressMap[addr];
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}, 30000);
|
|
252
|
+
if (sweepTimer.unref) sweepTimer.unref();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function close(callback) {
|
|
256
|
+
// Stop sweep
|
|
257
|
+
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
258
|
+
|
|
259
|
+
// Close all connections
|
|
260
|
+
for (var id in connections) {
|
|
261
|
+
try { connections[id].quic.close(0, 'server closing'); } catch (e) { }
|
|
262
|
+
}
|
|
263
|
+
connections = {};
|
|
264
|
+
addressMap = {};
|
|
265
|
+
|
|
266
|
+
var closed = 0;
|
|
267
|
+
var total = (udp4 ? 1 : 0) + (udp6 ? 1 : 0);
|
|
268
|
+
if (total === 0) { if (callback) callback(); return; }
|
|
269
|
+
|
|
270
|
+
function onClose() {
|
|
271
|
+
closed++;
|
|
272
|
+
if (closed >= total && typeof callback === 'function') callback();
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (udp4) { udp4.close(onClose); udp4 = null; }
|
|
276
|
+
if (udp6) { udp6.close(onClose); udp6 = null; }
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
// ============================================================
|
|
281
|
+
// External packet API (for shared UDP port scenarios)
|
|
282
|
+
// ============================================================
|
|
283
|
+
|
|
284
|
+
// Query whether the given 5-tuple matches an active QUIC connection.
|
|
285
|
+
// Used by the demuxer to disambiguate byte 0 in range 64-79 between
|
|
286
|
+
// QUIC short header and TURN ChannelData (RFC 9443). Fresh connections
|
|
287
|
+
// must be classified by byte 0 range (80-127 or 192+ = QUIC).
|
|
288
|
+
function hasConnection(rinfo) {
|
|
289
|
+
var key = rinfo.address + ':' + rinfo.port;
|
|
290
|
+
return (key in addressMap) && (addressMap[key] in connections);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
// ============================================================
|
|
295
|
+
// Server API
|
|
296
|
+
// ============================================================
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
listen: listen,
|
|
300
|
+
close: close,
|
|
301
|
+
on: function (name, fn) { ev.on(name, fn); },
|
|
302
|
+
|
|
303
|
+
/** Feed an incoming UDP packet when running in external-socket mode.
|
|
304
|
+
* rinfo must be in Node's dgram format: { address, port, family, size }. */
|
|
305
|
+
handlePacket: onUdpMessage,
|
|
306
|
+
|
|
307
|
+
/** Returns true if the 5-tuple matches an active QUIC connection.
|
|
308
|
+
* Used by demuxers for routing decisions on shared UDP ports. */
|
|
309
|
+
hasConnection: hasConnection
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
export { createQuicServer };
|
|
@@ -0,0 +1,76 @@
|
|
|
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 dgram from 'node:dgram';
|
|
16
|
+
import { QUICConnection } from './quic_connection.js';
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a UDP socket bound to an ephemeral port and wire it to a client-side
|
|
21
|
+
* QUICConnection. Shared by the HTTP/3 client (h3_client.js) and the
|
|
22
|
+
* WebTransport client (webtransport.js), which had near-identical setup code:
|
|
23
|
+
* pick udp4/udp6, create the socket, forward datagrams into the connection,
|
|
24
|
+
* bind, create the client QUICConnection, forward outgoing packets to the
|
|
25
|
+
* socket, and start the handshake.
|
|
26
|
+
*
|
|
27
|
+
* opts:
|
|
28
|
+
* remoteIp, remotePort — where to send packets
|
|
29
|
+
* hostname — SNI / :authority for the QUIC/TLS handshake
|
|
30
|
+
* onConnect(quic, socket) — called once the QUIC handshake completes
|
|
31
|
+
* onClose() — called when the connection closes (the socket is
|
|
32
|
+
* already closed by the time this runs)
|
|
33
|
+
* onError(err) — socket errors and outgoing-packet send errors
|
|
34
|
+
*
|
|
35
|
+
* Returns the UDP socket synchronously. The QUICConnection is created after the
|
|
36
|
+
* socket binds, so it is delivered via onConnect rather than returned here.
|
|
37
|
+
*/
|
|
38
|
+
function createQuicClientSocket(opts) {
|
|
39
|
+
var isIPv6 = opts.remoteIp.indexOf(':') >= 0;
|
|
40
|
+
var udpSocket = dgram.createSocket(isIPv6 ? 'udp6' : 'udp4');
|
|
41
|
+
var quic = null;
|
|
42
|
+
|
|
43
|
+
udpSocket.on('message', function (msg, rinfo) {
|
|
44
|
+
if (quic) quic.feedDatagram(rinfo.address, rinfo.port, new Uint8Array(msg));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
udpSocket.on('error', function (err) {
|
|
48
|
+
if (opts.onError) opts.onError(err);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
udpSocket.bind(0, function () {
|
|
52
|
+
quic = new QUICConnection({ isServer: false, hostname: opts.hostname });
|
|
53
|
+
|
|
54
|
+
quic.on('packet', function (data) {
|
|
55
|
+
udpSocket.send(data, opts.remotePort, opts.remoteIp, function (err) {
|
|
56
|
+
if (err && opts.onError) opts.onError(err);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
quic.on('connect', function () {
|
|
61
|
+
if (opts.onConnect) opts.onConnect(quic, udpSocket);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
quic.on('close', function () {
|
|
65
|
+
try { udpSocket.close(); } catch (e) {}
|
|
66
|
+
if (opts.onClose) opts.onClose();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
quic.connect();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return udpSocket;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
export { createQuicClientSocket };
|