quico 0.1.1 → 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/README.md +308 -70
- package/index.cjs +45 -0
- package/index.d.ts +158 -0
- package/index.js +46 -2631
- package/package.json +69 -14
- package/src/agent.js +275 -0
- package/src/client.js +439 -0
- package/src/crypto.js +519 -0
- package/src/h3.js +911 -0
- package/src/h3_client.js +317 -0
- package/src/h3_server.js +523 -0
- package/src/quic_connection.js +1238 -0
- package/src/server.js +346 -0
- package/src/streams.js +471 -0
- package/src/tls_bridge.js +211 -0
- package/src/transport.js +651 -0
- package/src/utils.js +290 -0
- package/src/webtransport.js +708 -0
- package/CHANGELOG.md +0 -25
- package/examples/webtransport_datagram_echo_server/client.html +0 -58
- package/examples/webtransport_datagram_echo_server/server.js +0 -29
- package/libs/crypto.js +0 -2107
- package/libs/h3.js +0 -1214
- package/libs/utils.js +0 -404
- package/quico.svg +0 -1
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 };
|