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