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/README.md +381 -216
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +49 -23
- package/package.json +83 -60
- package/src/agent.js +283 -0
- package/src/client.js +444 -0
- package/src/crypto.js +534 -0
- package/src/h3.js +920 -0
- package/src/h3_client.js +332 -0
- package/src/h3_server.js +392 -0
- package/src/quic_connection.js +2338 -0
- package/src/quic_server.js +314 -0
- package/src/quic_socket.js +76 -0
- package/src/server.js +354 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +273 -0
- package/src/transport.js +667 -0
- package/src/utils.js +345 -0
- package/src/webtransport.js +655 -0
- package/CHANGELOG.md +0 -31
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -23
- package/h3_server.js +0 -416
- package/h3_socket.js +0 -473
- package/libs/crypto.js +0 -1841
- package/libs/h3.js +0 -1461
- package/libs/utils.js +0 -404
- package/quic_socket.js +0 -1550
- package/quico.svg +0 -1
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 };
|