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/src/server.js ADDED
@@ -0,0 +1,354 @@
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 { EventEmitter } from 'node:events';
16
+ import fs from 'node:fs';
17
+ import http2 from 'node:http2';
18
+ import nodeHttps from 'node:https';
19
+ import nodeTls from 'node:tls';
20
+
21
+ import { createServer as createH3Server } from './h3_server.js';
22
+ import { IncomingMessage, ServerResponse, _makeSocketStub } from './streams.js';
23
+
24
+
25
+ // ============================================================
26
+ // createServer(options, handler)
27
+ //
28
+ // HTTP/3 is always enabled (that's the point of quico).
29
+ // Options:
30
+ // http2: true (default) — TCP with node:http2
31
+ // http1: true (default) — allowHTTP1 on TCP (or standalone if http2:false)
32
+ //
33
+ // Combinations:
34
+ // { } → H3 + H2 + H1 (default)
35
+ // { http1: false } → H3 + H2
36
+ // { http2: false } → H3 + H1
37
+ // { http2: false, http1: false } → H3 only
38
+ // ============================================================
39
+
40
+ function createServer(options, handler) {
41
+ if (typeof options === 'function') { handler = options; options = {}; }
42
+ options = options || {};
43
+
44
+ var enableH2 = options.http2 !== false;
45
+ var enableH1 = options.http1 !== false;
46
+ var needsTcp = enableH2 || enableH1;
47
+
48
+ var ev = new EventEmitter();
49
+ var port = null;
50
+
51
+ // ---- TLS credentials ----
52
+ var tlsKey = options.key || null;
53
+ var tlsCert = options.cert || null;
54
+ var tlsCa = options.ca || null;
55
+
56
+ // Read from disk if paths were given (not PEM strings)
57
+ if (typeof tlsKey === 'string' && tlsKey.indexOf('-----') === -1) {
58
+ tlsKey = fs.readFileSync(tlsKey);
59
+ }
60
+ if (typeof tlsCert === 'string' && tlsCert.indexOf('-----') === -1) {
61
+ tlsCert = fs.readFileSync(tlsCert);
62
+ }
63
+
64
+
65
+ // ============================================================
66
+ // HTTP/3 — delegates to existing h3_server.js
67
+ // No duplication: we pass a wrapper handler that converts
68
+ // plain req/res objects into proper Node.js streams.
69
+ // ============================================================
70
+
71
+ var h3Options = {
72
+ key: tlsKey,
73
+ cert: tlsCert,
74
+ ca: tlsCa,
75
+ SNICallback: options.SNICallback || null,
76
+ maxConnections: options.maxConnections || 10000,
77
+ // External UDP sockets (shared UDP port scenario). When provided,
78
+ // the UDP layer is owned externally — caller feeds packets via
79
+ // server.handlePacket(msg, rinfo). See RFC 9443.
80
+ socket: options.socket || null,
81
+ socket6: options.socket6 || null
82
+ };
83
+
84
+ var isExternalUdp = !!(options.socket || options.socket6);
85
+
86
+ var h3srv = createH3Server(h3Options, function (plainReq, plainRes) {
87
+ var socketStub = _makeSocketStub();
88
+ socketStub.remoteAddress = plainReq.remoteAddress || null;
89
+ socketStub.remotePort = plainReq.remotePort || null;
90
+
91
+ // ---- Wrap plain req into IncomingMessage (Readable) ----
92
+ var req = new IncomingMessage({
93
+ httpVersion: '3.0',
94
+ method: plainReq.method,
95
+ url: plainReq.url,
96
+ headers: plainReq.headers || {},
97
+ streamId: plainReq.stream_id,
98
+ socket: socketStub
99
+ });
100
+
101
+ // Bridge data from plain req → IncomingMessage
102
+ plainReq.on('data', function (chunk) { req._pushData(chunk); });
103
+ plainReq.on('end', function () { req._endData(); });
104
+
105
+ // ---- Wrap plain res into ServerResponse (Writable) ----
106
+ // ServerResponse._write → plainRes.write
107
+ // ServerResponse._flushHeaders → plainRes.writeHead
108
+ // ServerResponse._final → plainRes.end
109
+ var res = new ServerResponse({
110
+ streamId: plainReq.stream_id,
111
+ socket: socketStub
112
+ });
113
+ res.req = req;
114
+
115
+ // Store wrapper references for WebTransport event forwarding.
116
+ // MUST come after `res` is created (was previously set while res was
117
+ // still undefined due to var hoisting).
118
+ plainReq._wrapper = req;
119
+ plainRes._wrapper = res;
120
+
121
+ // Override internal methods to delegate to the plain H3 res.
122
+ // Key insight: we buffer one chunk behind so that _final can
123
+ // send the last data + FIN together via plainRes.end(chunk),
124
+ // instead of plainRes.write(chunk) + plainRes.end() separately.
125
+ // This avoids FIN-only packets that can get lost without PTO.
126
+ var _pendingChunk = null;
127
+
128
+ res._flushHeaders = function () {
129
+ if (res.headersSent) return;
130
+ plainRes.writeHead(res.statusCode, res._headers);
131
+ res.headersSent = true;
132
+ };
133
+
134
+ res._write = function (chunk, encoding, callback) {
135
+ if (!res.headersSent) res._flushHeaders();
136
+ if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
137
+
138
+ // Flush the previous chunk (not the current one — hold it for _final)
139
+ if (_pendingChunk !== null) {
140
+ plainRes.write(_pendingChunk);
141
+ }
142
+ _pendingChunk = chunk;
143
+ callback();
144
+ };
145
+
146
+ res._final = function (callback) {
147
+ if (!res.headersSent) res._flushHeaders();
148
+ if (_pendingChunk !== null) {
149
+ plainRes.end(_pendingChunk); // data + FIN together
150
+ } else {
151
+ plainRes.end(); // FIN only (no body written)
152
+ }
153
+ callback();
154
+ };
155
+
156
+ // ---- WebTransport method proxying ----
157
+ if (plainRes.sendDatagram) {
158
+ res.sendDatagram = plainRes.sendDatagram.bind(plainRes);
159
+ }
160
+ if (plainRes.createBidirectionalStream) {
161
+ res.createBidirectionalStream = plainRes.createBidirectionalStream.bind(plainRes);
162
+ }
163
+ if (plainRes.createUnidirectionalStream) {
164
+ res.createUnidirectionalStream = plainRes.createUnidirectionalStream.bind(plainRes);
165
+ }
166
+
167
+ if (typeof handler === 'function') handler(req, res);
168
+ ev.emit('request', req, res);
169
+ });
170
+
171
+
172
+ // ============================================================
173
+ // TCP — HTTP/2 and/or HTTP/1.1
174
+ // ============================================================
175
+
176
+ var tcpServer = null;
177
+
178
+ function startTcpServer(listenPort, host, callback) {
179
+ var hasTlsCredentials = (tlsKey && tlsCert) || options.SNICallback;
180
+ if (!needsTcp || !hasTlsCredentials) {
181
+ if (callback) callback();
182
+ return;
183
+ }
184
+
185
+ // Bridge SNICallback: detect context type and route accordingly.
186
+ // quico context (_nodeContext) → use pre-built node:tls context
187
+ // quico context (_rawKey) → build node:tls context from stored creds
188
+ // node:tls context → use directly (someone used tls.createSecureContext)
189
+ var tcpSniCallback = null;
190
+ if (options.SNICallback) {
191
+ tcpSniCallback = function (servername, cb) {
192
+ options.SNICallback(servername, function (err, ctx) {
193
+ if (err) return cb(err);
194
+ if (!ctx) return cb(new Error('SNICallback returned null'));
195
+
196
+ // 1. Pre-built node:tls context (fastest path)
197
+ if (ctx._nodeContext) {
198
+ return cb(null, ctx._nodeContext);
199
+ }
200
+ // 2. Raw key/cert stored — build node:tls context
201
+ if (ctx._rawKey && ctx._rawCert) {
202
+ var nodeOpts = { key: ctx._rawKey, cert: ctx._rawCert };
203
+ if (ctx._rawCa) nodeOpts.ca = ctx._rawCa;
204
+ try { return cb(null, nodeTls.createSecureContext(nodeOpts)); }
205
+ catch (e) { return cb(e); }
206
+ }
207
+ // 3. Assume it's already a node:tls context (tls.createSecureContext)
208
+ cb(null, ctx);
209
+ });
210
+ };
211
+ }
212
+
213
+ var altSvcHeader = 'h3=":' + listenPort + '"; ma=86400';
214
+
215
+ if (enableH2) {
216
+ // ---- HTTP/2 (+ optional HTTP/1.1) via node:http2 ----
217
+ var tcpOpts = {
218
+ allowHTTP1: enableH1,
219
+ settings: { enablePush: false }
220
+ };
221
+ if (tlsKey) tcpOpts.key = tlsKey;
222
+ if (tlsCert) tcpOpts.cert = tlsCert;
223
+ if (tlsCa) tcpOpts.ca = tlsCa;
224
+ if (tcpSniCallback) tcpOpts.SNICallback = tcpSniCallback;
225
+
226
+ tcpServer = http2.createSecureServer(tcpOpts);
227
+
228
+ tcpServer.on('request', function (req, res) {
229
+ res.setHeader('alt-svc', altSvcHeader);
230
+ if (typeof handler === 'function') handler(req, res);
231
+ ev.emit('request', req, res);
232
+ });
233
+
234
+ } else if (enableH1) {
235
+ // ---- HTTP/1.1 only via node:https ----
236
+ var tcpOpts = {};
237
+ if (tlsKey) tcpOpts.key = tlsKey;
238
+ if (tlsCert) tcpOpts.cert = tlsCert;
239
+ if (tlsCa) tcpOpts.ca = tlsCa;
240
+ if (tcpSniCallback) tcpOpts.SNICallback = tcpSniCallback;
241
+
242
+ tcpServer = nodeHttps.createServer(tcpOpts);
243
+
244
+ tcpServer.on('request', function (req, res) {
245
+ res.setHeader('alt-svc', altSvcHeader);
246
+ if (typeof handler === 'function') handler(req, res);
247
+ ev.emit('request', req, res);
248
+ });
249
+ }
250
+
251
+ if (tcpServer) {
252
+ tcpServer.on('error', function (err) { ev.emit('error', err); });
253
+ var tcpHost = host || '0.0.0.0';
254
+ tcpServer.listen(listenPort, tcpHost, callback);
255
+ } else {
256
+ if (callback) callback();
257
+ }
258
+ }
259
+
260
+
261
+ // ============================================================
262
+ // listen / close / address
263
+ // ============================================================
264
+
265
+ function listen(listenPort, host, callback) {
266
+ if (typeof host === 'function') { callback = host; host = null; }
267
+ port = listenPort || 443;
268
+
269
+ var remaining = needsTcp ? 2 : 1;
270
+ var called = false;
271
+
272
+ function done() {
273
+ remaining--;
274
+ if (remaining <= 0 && !called) {
275
+ called = true;
276
+ ev.emit('listening');
277
+ if (typeof callback === 'function') callback();
278
+ }
279
+ }
280
+
281
+ // H3 — always. In external mode h3srv.listen() wires up the external
282
+ // sockets' message handler and starts the sweep timer, but does not
283
+ // bind anything new.
284
+ h3srv.listen(port, host, done);
285
+
286
+ // TCP — if needed
287
+ if (needsTcp) {
288
+ startTcpServer(port, host, done);
289
+ }
290
+
291
+ return server;
292
+ }
293
+
294
+ function close(callback) {
295
+ var remaining = 1 + (tcpServer ? 1 : 0);
296
+
297
+ function onClose() {
298
+ remaining--;
299
+ if (remaining <= 0 && typeof callback === 'function') callback();
300
+ }
301
+
302
+ h3srv.close(onClose);
303
+ if (tcpServer) { tcpServer.close(onClose); tcpServer = null; }
304
+ else if (remaining <= 1 && typeof callback === 'function') callback();
305
+ }
306
+
307
+ function address() {
308
+ if (tcpServer && typeof tcpServer.address === 'function') {
309
+ var addr = tcpServer.address();
310
+ if (addr) return addr;
311
+ }
312
+ return { address: '0.0.0.0', family: 'IPv4', port: port };
313
+ }
314
+
315
+
316
+ // ============================================================
317
+ // Public API
318
+ // ============================================================
319
+
320
+ var server = {
321
+ listen: listen,
322
+ close: close,
323
+ address: address,
324
+ on: function (name, fn) { ev.on(name, fn); return server; },
325
+ once: function (name, fn) { ev.once(name, fn); return server; },
326
+ off: function (name, fn) { ev.off(name, fn); return server; },
327
+ removeListener: function (name, fn) { ev.removeListener(name, fn); return server; },
328
+ emit: function () { ev.emit.apply(ev, arguments); return server; },
329
+ setTimeout: function (msecs, callback) {
330
+ if (tcpServer && typeof tcpServer.setTimeout === 'function') {
331
+ tcpServer.setTimeout(msecs, callback);
332
+ }
333
+ return server;
334
+ },
335
+
336
+ /** Feed an incoming UDP packet from a shared/external socket.
337
+ * rinfo must be in Node's dgram format: { address, port, family, size }.
338
+ * Only relevant when the server was created with options.socket/socket6. */
339
+ handlePacket: function (msg, rinfo) {
340
+ return h3srv.handlePacket(msg, rinfo);
341
+ },
342
+
343
+ /** Returns true if the 5-tuple matches an active QUIC connection.
344
+ * Used by demuxers for routing decisions on shared UDP ports. */
345
+ hasConnection: function (rinfo) {
346
+ return h3srv.hasConnection(rinfo);
347
+ }
348
+ };
349
+
350
+ return server;
351
+ }
352
+
353
+
354
+ export { createServer };