quico 0.2.0 → 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/index.js +5 -2
- package/package.json +2 -2
- package/src/agent.js +9 -1
- package/src/client.js +6 -1
- package/src/crypto.js +20 -5
- package/src/h3.js +52 -43
- package/src/h3_client.js +93 -78
- package/src/h3_server.js +52 -183
- package/src/quic_connection.js +1221 -121
- package/src/quic_server.js +314 -0
- package/src/quic_socket.js +76 -0
- package/src/server.js +30 -22
- package/src/tls_bridge.js +74 -12
- package/src/transport.js +50 -34
- package/src/utils.js +59 -4
- package/src/webtransport.js +25 -78
package/src/h3_server.js
CHANGED
|
@@ -12,13 +12,10 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import dgram from 'node:dgram';
|
|
16
15
|
import { Duplex, Readable, Writable } from 'node:stream';
|
|
17
|
-
import { Emitter, readVarInt,
|
|
18
|
-
import {
|
|
19
|
-
import { QUICConnection } from './quic_connection.js';
|
|
16
|
+
import { Emitter, readVarInt, wt_datagram_payload } from './utils.js';
|
|
17
|
+
import { createQuicServer } from './quic_server.js';
|
|
20
18
|
import { H3Connection } from './h3.js';
|
|
21
|
-
import { createSecureContext } from './tls_bridge.js';
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
// ============================================================
|
|
@@ -29,23 +26,26 @@ function createServer(options, handler) {
|
|
|
29
26
|
options = options || {};
|
|
30
27
|
|
|
31
28
|
var ev = Emitter();
|
|
32
|
-
var udp4 = null;
|
|
33
|
-
var udp6 = null;
|
|
34
|
-
var port = null;
|
|
35
29
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
// Transport-only QUIC server owns the UDP socket(s), demux, connection
|
|
31
|
+
// map and lifecycle. h3_server layers HTTP/3 on top by listening for its
|
|
32
|
+
// 'connection' event (see below) and wrapping each raw QUICConnection in
|
|
33
|
+
// an H3Connection. ALPN is 'h3'. External-socket (shared-port) mode and
|
|
34
|
+
// the handlePacket/hasConnection demux hooks are provided by the
|
|
35
|
+
// transport server and re-exported on this server's API.
|
|
36
|
+
var qserver = createQuicServer({
|
|
37
|
+
alpn: 'h3',
|
|
38
|
+
SNICallback: options.SNICallback || null,
|
|
39
|
+
key: options.key,
|
|
40
|
+
cert: options.cert,
|
|
41
|
+
ca: options.ca,
|
|
42
|
+
maxConnections: options.maxConnections || 10000,
|
|
43
|
+
socket: options.socket || null,
|
|
44
|
+
socket6: options.socket6 || null
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Surface transport-level (UDP socket / send) errors on this server.
|
|
48
|
+
qserver.on('error', function (err) { ev.emit('error', err); });
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
// ============================================================
|
|
@@ -105,8 +105,7 @@ function createServer(options, handler) {
|
|
|
105
105
|
// Same pattern as old quico h3_server.js
|
|
106
106
|
// ============================================================
|
|
107
107
|
|
|
108
|
-
function set_http_stream(
|
|
109
|
-
var conn = connections[connId];
|
|
108
|
+
function set_http_stream(conn, streamId, params) {
|
|
110
109
|
if (!conn) return;
|
|
111
110
|
|
|
112
111
|
var is_new = false;
|
|
@@ -200,12 +199,7 @@ function createServer(options, handler) {
|
|
|
200
199
|
if (!req._isWebTransport) return;
|
|
201
200
|
if (typeof data === 'string') data = Buffer.from(data);
|
|
202
201
|
if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
203
|
-
|
|
204
|
-
var prefix = writeVarInt(quarterSid);
|
|
205
|
-
var frame = new Uint8Array(prefix.length + data.byteLength);
|
|
206
|
-
frame.set(prefix, 0);
|
|
207
|
-
frame.set(data, prefix.length);
|
|
208
|
-
conn.quic.sendDatagram(frame);
|
|
202
|
+
conn.quic.sendDatagram(wt_datagram_payload(Number(streamId), data));
|
|
209
203
|
},
|
|
210
204
|
|
|
211
205
|
createBidirectionalStream: function () {
|
|
@@ -284,93 +278,41 @@ function createServer(options, handler) {
|
|
|
284
278
|
|
|
285
279
|
|
|
286
280
|
// ============================================================
|
|
287
|
-
//
|
|
281
|
+
// HTTP/3 over each accepted QUIC connection
|
|
282
|
+
// The transport server hands us a raw QUICConnection per peer; we wrap
|
|
283
|
+
// it in an H3Connection once the handshake completes and bridge HTTP/3 +
|
|
284
|
+
// WebTransport events to req/res via set_http_stream. Per-connection
|
|
285
|
+
// state lives in this closure — the transport server owns the UDP
|
|
286
|
+
// socket, demux and connection lifecycle.
|
|
288
287
|
// ============================================================
|
|
289
288
|
|
|
290
|
-
function
|
|
291
|
-
var
|
|
292
|
-
var fromIp = rinfo.address;
|
|
293
|
-
var fromPort = rinfo.port;
|
|
294
|
-
console.log('[server] UDP from ' + fromIp + ':' + fromPort + ' len=' + data.length);
|
|
295
|
-
|
|
296
|
-
var addressKey = fromIp + ':' + fromPort;
|
|
297
|
-
var packets = parse_quic_datagram(data);
|
|
298
|
-
if (packets.length === 0) return;
|
|
299
|
-
|
|
300
|
-
for (var i = 0; i < packets.length; i++) {
|
|
301
|
-
var pkt = packets[i];
|
|
302
|
-
if (!pkt) continue;
|
|
303
|
-
|
|
304
|
-
var connId = findConnectionId(pkt, addressKey);
|
|
305
|
-
|
|
306
|
-
if (!(connId in connections)) {
|
|
307
|
-
// Reject new connections when at capacity
|
|
308
|
-
if (Object.keys(connections).length >= maxConnections) return;
|
|
309
|
-
createConnection(connId, fromIp, fromPort, addressKey);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
connections[connId].quic.feedDatagram(fromIp, fromPort, pkt.raw);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
function findConnectionId(pkt, addressKey) {
|
|
318
|
-
var dcidStr = null;
|
|
319
|
-
if (pkt.dcid && pkt.dcid.byteLength > 0) {
|
|
320
|
-
dcidStr = Array.from(pkt.dcid).join('');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (dcidStr && dcidStr in connections) return dcidStr;
|
|
324
|
-
if (addressKey in addressMap && addressMap[addressKey] in connections) return addressMap[addressKey];
|
|
325
|
-
|
|
326
|
-
var id = dcidStr || String(Math.floor(Math.random() * 9007199254740991));
|
|
327
|
-
addressMap[addressKey] = id;
|
|
328
|
-
return id;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
function createConnection(connId, fromIp, fromPort, addressKey) {
|
|
333
|
-
var quic = new QUICConnection({ SNICallback: sniCallback });
|
|
334
|
-
|
|
335
|
-
connections[connId] = {
|
|
336
|
-
quic: quic,
|
|
337
|
-
h3: null,
|
|
338
|
-
http_streams: {}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
addressMap[addressKey] = connId;
|
|
342
|
-
|
|
343
|
-
// Send packets back via UDP
|
|
344
|
-
quic.on('packet', function (data) {
|
|
345
|
-
sendUdp(fromIp, fromPort, data);
|
|
346
|
-
});
|
|
289
|
+
qserver.on('connection', function (quic, peer) {
|
|
290
|
+
var conn = { quic: quic, h3: null, http_streams: {} };
|
|
347
291
|
|
|
348
292
|
// QUIC handshake complete → set up H3
|
|
349
293
|
quic.on('connect', function () {
|
|
350
294
|
var h3 = new H3Connection({ quicConnection: quic, enableWebTransport: true });
|
|
351
|
-
|
|
295
|
+
conn.h3 = h3;
|
|
352
296
|
|
|
353
297
|
// HTTP request headers received → set_http_stream (reactive)
|
|
354
298
|
h3.on('http_headers', function (streamId, headers) {
|
|
355
|
-
set_http_stream(
|
|
299
|
+
set_http_stream(conn, streamId, { request_headers: headers });
|
|
356
300
|
});
|
|
357
301
|
|
|
358
302
|
// HTTP request body chunk → set_http_stream
|
|
359
303
|
h3.on('http_body', function (streamId, data) {
|
|
360
|
-
set_http_stream(
|
|
304
|
+
set_http_stream(conn, streamId, { request_body_chunk: data });
|
|
361
305
|
});
|
|
362
306
|
|
|
363
307
|
// HTTP request complete → fire req.on('end')
|
|
364
308
|
h3.on('http_end', function (streamId) {
|
|
365
|
-
set_http_stream(
|
|
309
|
+
set_http_stream(conn, streamId, { request_end: true });
|
|
366
310
|
});
|
|
367
311
|
|
|
368
312
|
// ---- WebTransport stream events ----
|
|
369
313
|
|
|
370
314
|
// New WT stream (bidi or uni from client)
|
|
371
315
|
h3.on('wt_stream', function (sessionId, wtStreamId, data, fin, isBidi) {
|
|
372
|
-
var conn = connections[connId];
|
|
373
|
-
if (!conn) return;
|
|
374
316
|
var session = conn.http_streams[sessionId];
|
|
375
317
|
if (!session || !session.req._isWebTransport) return;
|
|
376
318
|
|
|
@@ -395,8 +337,7 @@ function createServer(options, handler) {
|
|
|
395
337
|
|
|
396
338
|
// Subsequent data on WT stream
|
|
397
339
|
h3.on('wt_data', function (sessionId, wtStreamId, data, fin) {
|
|
398
|
-
|
|
399
|
-
if (!conn || !conn._wtStreamObjects) return;
|
|
340
|
+
if (!conn._wtStreamObjects) return;
|
|
400
341
|
var wtStream = conn._wtStreamObjects[wtStreamId];
|
|
401
342
|
if (!wtStream) return;
|
|
402
343
|
if (data.byteLength > 0) wtStream._pushData(data);
|
|
@@ -404,7 +345,7 @@ function createServer(options, handler) {
|
|
|
404
345
|
});
|
|
405
346
|
|
|
406
347
|
// Datagrams
|
|
407
|
-
quic.on('datagram', function (
|
|
348
|
+
quic.on('datagram', function (rawData) {
|
|
408
349
|
if (!rawData || rawData.byteLength === 0) return;
|
|
409
350
|
var result = readVarInt(rawData, 0);
|
|
410
351
|
if (!result) return;
|
|
@@ -412,8 +353,6 @@ function createServer(options, handler) {
|
|
|
412
353
|
var payload = rawData.slice(result.byteLength);
|
|
413
354
|
|
|
414
355
|
// Find the session: quarter_stream_id = session_stream_id / 4
|
|
415
|
-
var conn = connections[connId];
|
|
416
|
-
if (!conn) return;
|
|
417
356
|
for (var sid in conn.http_streams) {
|
|
418
357
|
var session = conn.http_streams[sid];
|
|
419
358
|
if (session.req._isWebTransport && Math.floor(Number(sid) / 4) === quarterSid) {
|
|
@@ -423,99 +362,29 @@ function createServer(options, handler) {
|
|
|
423
362
|
}
|
|
424
363
|
});
|
|
425
364
|
});
|
|
426
|
-
|
|
427
|
-
quic.on('close', function () {
|
|
428
|
-
delete connections[connId];
|
|
429
|
-
if (addressMap[addressKey] === connId) delete addressMap[addressKey];
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// ============================================================
|
|
435
|
-
// UDP send
|
|
436
|
-
// ============================================================
|
|
437
|
-
|
|
438
|
-
function sendUdp(toIp, toPort, data) {
|
|
439
|
-
var socket = toIp.indexOf(':') >= 0 ? udp6 : udp4;
|
|
440
|
-
if (!socket) return;
|
|
441
|
-
socket.send(data, toPort, toIp, function (err) {
|
|
442
|
-
if (err) ev.emit('error', err);
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// ============================================================
|
|
448
|
-
// listen / close
|
|
449
|
-
// ============================================================
|
|
450
|
-
|
|
451
|
-
function listen(listenPort, host, callback) {
|
|
452
|
-
if (typeof host === 'function') { callback = host; host = null; }
|
|
453
|
-
port = listenPort || 443;
|
|
454
|
-
host = host || '::';
|
|
455
|
-
|
|
456
|
-
udp4 = dgram.createSocket('udp4');
|
|
457
|
-
udp4.on('message', onUdpMessage);
|
|
458
|
-
udp4.on('error', function (err) { ev.emit('error', err); });
|
|
459
|
-
var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
|
|
460
|
-
udp4.bind(port, host4);
|
|
461
|
-
|
|
462
|
-
udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
|
|
463
|
-
udp6.on('message', onUdpMessage);
|
|
464
|
-
udp6.on('error', function (err) { ev.emit('error', err); });
|
|
465
|
-
var host6 = host.indexOf(':') !== -1 ? host : '::';
|
|
466
|
-
udp6.bind(port, host6, function () {
|
|
467
|
-
if (typeof callback === 'function') callback();
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// Periodic sweep for dead connections (safety net)
|
|
471
|
-
sweepTimer = setInterval(function () {
|
|
472
|
-
for (var id in connections) {
|
|
473
|
-
var st = connections[id].quic.state;
|
|
474
|
-
if (st === 'closed') {
|
|
475
|
-
delete connections[id];
|
|
476
|
-
// Clean address map
|
|
477
|
-
for (var addr in addressMap) {
|
|
478
|
-
if (addressMap[addr] === id) delete addressMap[addr];
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}, 30000);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function close(callback) {
|
|
486
|
-
// Stop sweep
|
|
487
|
-
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
488
|
-
|
|
489
|
-
// Close all connections
|
|
490
|
-
for (var id in connections) {
|
|
491
|
-
try { connections[id].quic.close(0, 'server closing'); } catch (e) { }
|
|
492
|
-
}
|
|
493
|
-
connections = {};
|
|
494
|
-
addressMap = {};
|
|
495
|
-
|
|
496
|
-
var closed = 0;
|
|
497
|
-
var total = (udp4 ? 1 : 0) + (udp6 ? 1 : 0);
|
|
498
|
-
if (total === 0) { if (callback) callback(); return; }
|
|
499
|
-
|
|
500
|
-
function onClose() {
|
|
501
|
-
closed++;
|
|
502
|
-
if (closed >= total && typeof callback === 'function') callback();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (udp4) { udp4.close(onClose); udp4 = null; }
|
|
506
|
-
if (udp6) { udp6.close(onClose); udp6 = null; }
|
|
507
|
-
}
|
|
365
|
+
});
|
|
508
366
|
|
|
509
367
|
|
|
510
368
|
// ============================================================
|
|
511
369
|
// Server API
|
|
370
|
+
// Transport concerns (UDP sockets, demux, connection lifecycle,
|
|
371
|
+
// shared-port handlePacket/hasConnection) are delegated to the QUIC
|
|
372
|
+
// server. This layer adds HTTP/3 request events on top.
|
|
512
373
|
// ============================================================
|
|
513
374
|
|
|
514
375
|
return {
|
|
515
|
-
listen: listen,
|
|
516
|
-
close: close,
|
|
376
|
+
listen: qserver.listen,
|
|
377
|
+
close: qserver.close,
|
|
517
378
|
on: function (name, fn) { ev.on(name, fn); },
|
|
518
|
-
setTimeout: function () { /* TODO */ }
|
|
379
|
+
setTimeout: function () { /* TODO */ },
|
|
380
|
+
|
|
381
|
+
/** Feed an incoming UDP packet when running in external-socket mode.
|
|
382
|
+
* rinfo must be in Node's dgram format: { address, port, family, size }. */
|
|
383
|
+
handlePacket: qserver.handlePacket,
|
|
384
|
+
|
|
385
|
+
/** Returns true if the 5-tuple matches an active QUIC connection.
|
|
386
|
+
* Used by demuxers for routing decisions on shared UDP ports. */
|
|
387
|
+
hasConnection: qserver.hasConnection
|
|
519
388
|
};
|
|
520
389
|
}
|
|
521
390
|
|