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/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, writeVarInt } from './utils.js';
18
- import { parse_quic_datagram } from './transport.js';
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
- // Build SNICallback
37
- var sniCallback = options.SNICallback || null;
38
-
39
- if (!sniCallback && options.key && options.cert) {
40
- var defaultCtx = createSecureContext({ key: options.key, cert: options.cert, ca: options.ca });
41
- sniCallback = function (servername, cb) { cb(null, defaultCtx); };
42
- }
43
-
44
- // ---- Connection tracking ----
45
- var connections = {}; // connId → { quic, h3, http_streams }
46
- var addressMap = {}; // "ip:port" → connId
47
- var maxConnections = options.maxConnections || 10000;
48
- var sweepTimer = null;
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(connId, streamId, params) {
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
- var quarterSid = Math.floor(Number(streamId) / 4);
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
- // UDP → connection routing
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 onUdpMessage(msg, rinfo) {
291
- var data = new Uint8Array(msg);
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
- connections[connId].h3 = h3;
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(connId, streamId, { request_headers: headers });
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(connId, streamId, { request_body_chunk: data });
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(connId, streamId, { request_end: true });
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
- var conn = connections[connId];
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 (_contextId, rawData) {
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