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.
@@ -0,0 +1,392 @@
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 { Duplex, Readable, Writable } from 'node:stream';
16
+ import { Emitter, readVarInt, wt_datagram_payload } from './utils.js';
17
+ import { createQuicServer } from './quic_server.js';
18
+ import { H3Connection } from './h3.js';
19
+
20
+
21
+ // ============================================================
22
+ // createServer(options, handler)
23
+ // ============================================================
24
+
25
+ function createServer(options, handler) {
26
+ options = options || {};
27
+
28
+ var ev = Emitter();
29
+
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
+
50
+
51
+ // ============================================================
52
+ // WebTransport helpers
53
+ // ============================================================
54
+
55
+ function _emitOnReq(req, event, data) {
56
+ // If unified server wrapped the req, emit on the wrapper (IncomingMessage/EventEmitter)
57
+ if (req._wrapper && typeof req._wrapper.emit === 'function') {
58
+ req._wrapper.emit(event, data);
59
+ return;
60
+ }
61
+ // Fallback: emit on raw req's _listeners
62
+ var listeners = req._listeners[event] || [];
63
+ for (var i = 0; i < listeners.length; i++) {
64
+ try { if (data !== undefined) listeners[i](data); else listeners[i](); } catch (e) {}
65
+ }
66
+ }
67
+
68
+ function _createWtStream(conn, quic, wtStreamId, sessionId, isBidi) {
69
+ if (!conn._wtStreamObjects) conn._wtStreamObjects = {};
70
+
71
+ var stream;
72
+ if (isBidi) {
73
+ stream = new Duplex({
74
+ read: function () {},
75
+ write: function (chunk, encoding, cb) {
76
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
77
+ quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
78
+ cb();
79
+ },
80
+ final: function (cb) {
81
+ quic.sendStream(wtStreamId, new Uint8Array(0), true);
82
+ cb();
83
+ }
84
+ });
85
+ } else {
86
+ stream = new Readable({ read: function () {} });
87
+ }
88
+
89
+ stream._streamId = wtStreamId;
90
+ stream._sessionId = sessionId;
91
+ stream._pushData = function (data) {
92
+ if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
93
+ stream.push(data);
94
+ };
95
+ stream._endData = function () { stream.push(null); };
96
+
97
+ conn._wtStreamObjects[wtStreamId] = stream;
98
+ return stream;
99
+ }
100
+
101
+
102
+ // ============================================================
103
+ // set_http_stream — reactive HTTP stream builder
104
+ // Creates req/res on first call, fires handler when ready.
105
+ // Same pattern as old quico h3_server.js
106
+ // ============================================================
107
+
108
+ function set_http_stream(conn, streamId, params) {
109
+ if (!conn) return;
110
+
111
+ var is_new = false;
112
+
113
+ // Create req/res on first encounter of this stream
114
+ if (!(streamId in conn.http_streams)) {
115
+
116
+ var req = {
117
+ method: null,
118
+ url: null,
119
+ headers: {},
120
+ httpVersion: '3.0',
121
+ stream_id: Number(streamId),
122
+ _listeners: {},
123
+ _isWebTransport: false,
124
+
125
+ on: function (event, fn) {
126
+ (req._listeners[event] = req._listeners[event] || []).push(fn);
127
+ }
128
+ };
129
+
130
+ var res = {
131
+ statusCode: 200,
132
+ headers: {},
133
+ headersSent: false,
134
+ _finished: false,
135
+
136
+ setHeader: function (name, value) {
137
+ res.headers[name.toLowerCase()] = String(value);
138
+ },
139
+
140
+ getHeader: function (name) {
141
+ return res.headers[name.toLowerCase()];
142
+ },
143
+
144
+ writeHead: function (statusCode, headers) {
145
+ if (res.headersSent) return res;
146
+ res.statusCode = statusCode;
147
+
148
+ if (typeof headers === 'object') {
149
+ for (var name in headers) {
150
+ res.headers[name.toLowerCase()] = String(headers[name]);
151
+ }
152
+ }
153
+
154
+ var h3Headers = { ':status': String(statusCode) };
155
+ for (var name in res.headers) {
156
+ h3Headers[name] = res.headers[name];
157
+ }
158
+
159
+ if (conn.h3) {
160
+ // Send headers WITHOUT FIN for WT (session stays open)
161
+ var isWt = req.headers[':protocol'] === 'webtransport';
162
+ conn.h3.sendHeaders(streamId, h3Headers, isWt ? false : undefined);
163
+ }
164
+ res.headersSent = true;
165
+
166
+ // If this is a WT CONNECT with 200 → register session
167
+ if (req.headers[':protocol'] === 'webtransport' && statusCode === 200 && conn.h3) {
168
+ req._isWebTransport = true;
169
+ conn.h3.registerWebTransportSession(Number(streamId));
170
+ }
171
+
172
+ return res;
173
+ },
174
+
175
+ write: function (chunk) {
176
+ if (!res.headersSent) res.writeHead(res.statusCode);
177
+ if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
178
+ if (conn.h3) conn.h3.sendBody(streamId, chunk, false);
179
+ return true;
180
+ },
181
+
182
+ end: function (chunk) {
183
+ if (res._finished) return;
184
+ if (!res.headersSent) res.writeHead(res.statusCode);
185
+ if (conn.h3) {
186
+ if (chunk) {
187
+ if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
188
+ conn.h3.sendBody(streamId, chunk, true);
189
+ } else {
190
+ conn.h3.sendBody(streamId, null, true);
191
+ }
192
+ }
193
+ res._finished = true;
194
+ },
195
+
196
+ // ---- WebTransport methods on res ----
197
+
198
+ sendDatagram: function (data) {
199
+ if (!req._isWebTransport) return;
200
+ if (typeof data === 'string') data = Buffer.from(data);
201
+ if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
202
+ conn.quic.sendDatagram(wt_datagram_payload(Number(streamId), data));
203
+ },
204
+
205
+ createBidirectionalStream: function () {
206
+ if (!req._isWebTransport || !conn.h3) return null;
207
+ var wtStreamId = conn.h3.createWebTransportStream(Number(streamId), true);
208
+ return _createWtStream(conn, conn.quic, wtStreamId, Number(streamId), true);
209
+ },
210
+
211
+ createUnidirectionalStream: function () {
212
+ if (!req._isWebTransport || !conn.h3) return null;
213
+ var wtStreamId = conn.h3.createWebTransportStream(Number(streamId), false);
214
+ var stream = new Writable({
215
+ write: function (chunk, encoding, cb) {
216
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
217
+ conn.quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
218
+ cb();
219
+ },
220
+ final: function (cb) {
221
+ conn.quic.sendStream(wtStreamId, new Uint8Array(0), true);
222
+ cb();
223
+ }
224
+ });
225
+ stream._streamId = wtStreamId;
226
+ return stream;
227
+ }
228
+ };
229
+
230
+ conn.http_streams[streamId] = { req: req, res: res };
231
+ is_new = true;
232
+ }
233
+
234
+ // Apply params
235
+ if (typeof params === 'object') {
236
+
237
+ if ('request_headers' in params) {
238
+ var stream = conn.http_streams[streamId];
239
+ for (var name in params.request_headers) {
240
+ stream.req.headers[name] = params.request_headers[name];
241
+ }
242
+
243
+ // Extract :method and :path
244
+ if (stream.req.method === null && ':method' in stream.req.headers) {
245
+ stream.req.method = stream.req.headers[':method'];
246
+ }
247
+ if (stream.req.url === null && ':path' in stream.req.headers) {
248
+ stream.req.url = stream.req.headers[':path'];
249
+ }
250
+ }
251
+
252
+ if ('request_body_chunk' in params) {
253
+ var stream = conn.http_streams[streamId];
254
+ var listeners = stream.req._listeners['data'] || [];
255
+ for (var i = 0; i < listeners.length; i++) {
256
+ try { listeners[i](params.request_body_chunk); } catch (e) { }
257
+ }
258
+ }
259
+
260
+ if ('request_end' in params) {
261
+ var stream = conn.http_streams[streamId];
262
+ var listeners = stream.req._listeners['end'] || [];
263
+ for (var i = 0; i < listeners.length; i++) {
264
+ try { listeners[i](); } catch (e) { }
265
+ }
266
+ }
267
+ }
268
+
269
+ // Fire handler on new stream (after headers are set)
270
+ if (is_new) {
271
+ var stream = conn.http_streams[streamId];
272
+ if (typeof handler === 'function') {
273
+ handler(stream.req, stream.res);
274
+ }
275
+ ev.emit('request', stream.req, stream.res);
276
+ }
277
+ }
278
+
279
+
280
+ // ============================================================
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.
287
+ // ============================================================
288
+
289
+ qserver.on('connection', function (quic, peer) {
290
+ var conn = { quic: quic, h3: null, http_streams: {} };
291
+
292
+ // QUIC handshake complete → set up H3
293
+ quic.on('connect', function () {
294
+ var h3 = new H3Connection({ quicConnection: quic, enableWebTransport: true });
295
+ conn.h3 = h3;
296
+
297
+ // HTTP request headers received → set_http_stream (reactive)
298
+ h3.on('http_headers', function (streamId, headers) {
299
+ set_http_stream(conn, streamId, { request_headers: headers });
300
+ });
301
+
302
+ // HTTP request body chunk → set_http_stream
303
+ h3.on('http_body', function (streamId, data) {
304
+ set_http_stream(conn, streamId, { request_body_chunk: data });
305
+ });
306
+
307
+ // HTTP request complete → fire req.on('end')
308
+ h3.on('http_end', function (streamId) {
309
+ set_http_stream(conn, streamId, { request_end: true });
310
+ });
311
+
312
+ // ---- WebTransport stream events ----
313
+
314
+ // New WT stream (bidi or uni from client)
315
+ h3.on('wt_stream', function (sessionId, wtStreamId, data, fin, isBidi) {
316
+ var session = conn.http_streams[sessionId];
317
+ if (!session || !session.req._isWebTransport) return;
318
+
319
+ // Create a Duplex-like stream for the WT stream
320
+ var wtStream = _createWtStream(conn, quic, wtStreamId, sessionId, isBidi);
321
+
322
+ // Push initial data
323
+ if (data.byteLength > 0) _emitOnReq(session.req, 'data:' + wtStreamId, data);
324
+ if (fin) _emitOnReq(session.req, 'end:' + wtStreamId);
325
+
326
+ // Emit on req
327
+ if (isBidi) {
328
+ _emitOnReq(session.req, 'stream', wtStream);
329
+ } else {
330
+ _emitOnReq(session.req, 'unidirectionalStream', wtStream);
331
+ }
332
+
333
+ // If initial data, push to stream
334
+ if (data.byteLength > 0) wtStream._pushData(data);
335
+ if (fin) wtStream._endData();
336
+ });
337
+
338
+ // Subsequent data on WT stream
339
+ h3.on('wt_data', function (sessionId, wtStreamId, data, fin) {
340
+ if (!conn._wtStreamObjects) return;
341
+ var wtStream = conn._wtStreamObjects[wtStreamId];
342
+ if (!wtStream) return;
343
+ if (data.byteLength > 0) wtStream._pushData(data);
344
+ if (fin) { wtStream._endData(); delete conn._wtStreamObjects[wtStreamId]; }
345
+ });
346
+
347
+ // Datagrams
348
+ quic.on('datagram', function (rawData) {
349
+ if (!rawData || rawData.byteLength === 0) return;
350
+ var result = readVarInt(rawData, 0);
351
+ if (!result) return;
352
+ var quarterSid = result.value;
353
+ var payload = rawData.slice(result.byteLength);
354
+
355
+ // Find the session: quarter_stream_id = session_stream_id / 4
356
+ for (var sid in conn.http_streams) {
357
+ var session = conn.http_streams[sid];
358
+ if (session.req._isWebTransport && Math.floor(Number(sid) / 4) === quarterSid) {
359
+ _emitOnReq(session.req, 'datagram', Buffer.from(payload));
360
+ break;
361
+ }
362
+ }
363
+ });
364
+ });
365
+ });
366
+
367
+
368
+ // ============================================================
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.
373
+ // ============================================================
374
+
375
+ return {
376
+ listen: qserver.listen,
377
+ close: qserver.close,
378
+ on: function (name, fn) { ev.on(name, fn); },
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
388
+ };
389
+ }
390
+
391
+
392
+ export { createServer };