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/package.json
CHANGED
|
@@ -1,28 +1,83 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quico",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.cjs",
|
|
7
|
+
"module": "index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./index.js",
|
|
11
|
+
"require": "./index.cjs"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"types": "index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"index.cjs",
|
|
21
|
+
"index.d.ts",
|
|
22
|
+
"src/",
|
|
23
|
+
"LICENSE",
|
|
24
|
+
"README.md"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"quic",
|
|
28
|
+
"http3",
|
|
29
|
+
"http",
|
|
30
|
+
"https",
|
|
31
|
+
"h3",
|
|
32
|
+
"quic-node",
|
|
33
|
+
"http3-node",
|
|
34
|
+
"quic-server",
|
|
35
|
+
"quic-client",
|
|
36
|
+
"http3-server",
|
|
37
|
+
"http3-client",
|
|
38
|
+
"h3-node",
|
|
39
|
+
"webtransport",
|
|
40
|
+
"websocket",
|
|
41
|
+
"datagram",
|
|
42
|
+
"udp",
|
|
43
|
+
"qpack",
|
|
44
|
+
"tls",
|
|
45
|
+
"tls1.3",
|
|
46
|
+
"tls13",
|
|
47
|
+
"nodejs",
|
|
48
|
+
"backend",
|
|
49
|
+
"real-time",
|
|
50
|
+
"low-latency",
|
|
51
|
+
"transport",
|
|
52
|
+
"http-server",
|
|
53
|
+
"socketio"
|
|
54
|
+
],
|
|
55
|
+
"author": {
|
|
56
|
+
"name": "colocohen",
|
|
57
|
+
"email": "support@quicojs.dev"
|
|
8
58
|
},
|
|
9
|
-
"keywords": ["quic", "http3", "h3", "webtransport", "datagram", "udp", "qpack", "tls", "tls1.3", "nodejs"],
|
|
10
|
-
"author": "colocohen",
|
|
11
59
|
"license": "Apache-2.0",
|
|
12
60
|
"repository": {
|
|
13
61
|
"type": "git",
|
|
14
62
|
"url": "https://github.com/colocohen/quico.git"
|
|
15
63
|
},
|
|
16
64
|
"bugs": {
|
|
65
|
+
"email": "support@quicojs.dev",
|
|
17
66
|
"url": "https://github.com/colocohen/quico/issues"
|
|
18
67
|
},
|
|
19
|
-
"homepage": "https://github.com/colocohen/quico",
|
|
68
|
+
"homepage": "https://github.com/colocohen/quico#readme",
|
|
69
|
+
"funding": [
|
|
70
|
+
{
|
|
71
|
+
"type": "github",
|
|
72
|
+
"url": "https://github.com/sponsors/colocohen"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"type": "opencollective",
|
|
76
|
+
"url": "https://opencollective.com/quicojs"
|
|
77
|
+
}
|
|
78
|
+
],
|
|
20
79
|
"dependencies": {
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"@peculiar/x509": ">=1.13.0",
|
|
24
|
-
"@stablelib/aes": ">=2.0.1",
|
|
25
|
-
"@stablelib/gcm": ">=2.0.1",
|
|
26
|
-
"flat-ranges": ">=1.0.2"
|
|
80
|
+
"flat-ranges": "^2.0.0",
|
|
81
|
+
"lemon-tls": "^0.2.0"
|
|
27
82
|
}
|
|
28
|
-
}
|
|
83
|
+
}
|
package/src/agent.js
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
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
|
+
|
|
17
|
+
|
|
18
|
+
// ============================================================
|
|
19
|
+
// Agent — connection pooling + protocol discovery
|
|
20
|
+
//
|
|
21
|
+
// Mirrors node:https.Agent API surface.
|
|
22
|
+
// Caches which protocol works per host so subsequent requests
|
|
23
|
+
// skip the discovery step.
|
|
24
|
+
//
|
|
25
|
+
// Options:
|
|
26
|
+
// http2: true (default) — allow HTTP/2 in fallback chain
|
|
27
|
+
// http1: true (default) — allow HTTP/1.1 in fallback chain
|
|
28
|
+
// h3Timeout: 3000 — ms before falling back from H3
|
|
29
|
+
// ============================================================
|
|
30
|
+
|
|
31
|
+
class Agent extends EventEmitter {
|
|
32
|
+
|
|
33
|
+
constructor(options) {
|
|
34
|
+
super();
|
|
35
|
+
|
|
36
|
+
options = options || {};
|
|
37
|
+
|
|
38
|
+
this.keepAlive = options.keepAlive !== false;
|
|
39
|
+
this.keepAliveMsecs = options.keepAliveMsecs || 1000;
|
|
40
|
+
this.maxSockets = options.maxSockets || 256;
|
|
41
|
+
this.maxFreeSockets = options.maxFreeSockets || 32;
|
|
42
|
+
this.timeout = options.timeout || 30000;
|
|
43
|
+
|
|
44
|
+
// Protocol flags (defaults for all requests through this agent)
|
|
45
|
+
this.http2 = options.http2 !== false;
|
|
46
|
+
this.http1 = options.http1 !== false;
|
|
47
|
+
|
|
48
|
+
// H3 connect timeout before falling back (ms)
|
|
49
|
+
this.h3Timeout = options.h3Timeout || 3000;
|
|
50
|
+
|
|
51
|
+
// Protocol cache: "host:port" → { protocol, ts, altSvcMaxAge }
|
|
52
|
+
// protocol: 'h3' | 'h2' | 'https'
|
|
53
|
+
this._protocolCache = {};
|
|
54
|
+
|
|
55
|
+
// Active QUIC connections: "host:port" → QUICConnection
|
|
56
|
+
// Allows multiplexing multiple requests over one QUIC connection
|
|
57
|
+
this._h3Pool = {};
|
|
58
|
+
|
|
59
|
+
// Active HTTP/2 sessions: "host:port" → http2.ClientHttp2Session
|
|
60
|
+
this._h2Pool = {};
|
|
61
|
+
|
|
62
|
+
// Request count per host (for maxSockets enforcement)
|
|
63
|
+
this._activeCounts = {};
|
|
64
|
+
|
|
65
|
+
// Sweep interval — clean stale pool entries
|
|
66
|
+
this._sweepTimer = setInterval(() => this._sweep(), 15000);
|
|
67
|
+
if (this._sweepTimer.unref) this._sweepTimer.unref();
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
// ============================================================
|
|
72
|
+
// Protocol cache
|
|
73
|
+
// ============================================================
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get cached protocol for a host, or null if unknown.
|
|
77
|
+
*/
|
|
78
|
+
getProtocol(host, port) {
|
|
79
|
+
var key = host + ':' + (port || 443);
|
|
80
|
+
var entry = this._protocolCache[key];
|
|
81
|
+
if (!entry) return null;
|
|
82
|
+
|
|
83
|
+
// Check expiry (Alt-Svc max-age, default 24h)
|
|
84
|
+
var maxAge = entry.altSvcMaxAge || 86400000;
|
|
85
|
+
if (Date.now() - entry.ts > maxAge) {
|
|
86
|
+
delete this._protocolCache[key];
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return entry.protocol;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record which protocol works for a host.
|
|
95
|
+
*/
|
|
96
|
+
setProtocol(host, port, protocol, altSvcMaxAge) {
|
|
97
|
+
var key = host + ':' + (port || 443);
|
|
98
|
+
this._protocolCache[key] = {
|
|
99
|
+
protocol: protocol,
|
|
100
|
+
ts: Date.now(),
|
|
101
|
+
altSvcMaxAge: altSvcMaxAge || 86400000
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
// ============================================================
|
|
107
|
+
// Alt-Svc header parsing
|
|
108
|
+
// ============================================================
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse Alt-Svc header from an HTTP/1.1 or HTTP/2 response.
|
|
112
|
+
* If it advertises h3, cache it for future requests.
|
|
113
|
+
*
|
|
114
|
+
* Examples:
|
|
115
|
+
* Alt-Svc: h3=":443"; ma=86400
|
|
116
|
+
* Alt-Svc: h3=":443", h3-29=":443"
|
|
117
|
+
*/
|
|
118
|
+
parseAltSvc(host, port, altSvcValue) {
|
|
119
|
+
if (!altSvcValue || altSvcValue === 'clear') {
|
|
120
|
+
var key = host + ':' + (port || 443);
|
|
121
|
+
delete this._protocolCache[key];
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
var entries = altSvcValue.split(',');
|
|
126
|
+
for (var i = 0; i < entries.length; i++) {
|
|
127
|
+
var entry = entries[i].trim();
|
|
128
|
+
var match = entry.match(/^h3(?:-\d+)?="([^"]*)"(.*)$/);
|
|
129
|
+
if (match) {
|
|
130
|
+
var authority = match[1];
|
|
131
|
+
var params = match[2] || '';
|
|
132
|
+
|
|
133
|
+
var maMatch = params.match(/ma=(\d+)/);
|
|
134
|
+
var maxAge = maMatch ? parseInt(maMatch[1]) * 1000 : 86400000;
|
|
135
|
+
|
|
136
|
+
var h3Port = port;
|
|
137
|
+
var colonIdx = authority.lastIndexOf(':');
|
|
138
|
+
if (colonIdx >= 0) {
|
|
139
|
+
var parsed = parseInt(authority.substring(colonIdx + 1));
|
|
140
|
+
if (!isNaN(parsed)) h3Port = parsed;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
this.setProtocol(host, h3Port, 'h3', maxAge);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
// ============================================================
|
|
151
|
+
// H3 connection pool
|
|
152
|
+
// Stores { quic, h3, udpSocket, _nextStreamId } objects
|
|
153
|
+
// from h3_client.js for multiplexing.
|
|
154
|
+
// ============================================================
|
|
155
|
+
|
|
156
|
+
getH3Connection(host, port) {
|
|
157
|
+
var key = host + ':' + (port || 443);
|
|
158
|
+
var conn = this._h3Pool[key];
|
|
159
|
+
if (!conn) return null;
|
|
160
|
+
var state = conn.quic ? conn.quic.state : 'closed';
|
|
161
|
+
if (state === 'closed' || state === 'draining') {
|
|
162
|
+
delete this._h3Pool[key];
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
return conn;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
setH3Connection(host, port, conn) {
|
|
169
|
+
var key = host + ':' + (port || 443);
|
|
170
|
+
this._h3Pool[key] = conn;
|
|
171
|
+
if (conn.quic) {
|
|
172
|
+
conn.quic.on('close', () => {
|
|
173
|
+
if (this._h3Pool[key] === conn) delete this._h3Pool[key];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
// ============================================================
|
|
180
|
+
// H2 session pool
|
|
181
|
+
// ============================================================
|
|
182
|
+
|
|
183
|
+
getH2Session(host, port) {
|
|
184
|
+
var key = host + ':' + (port || 443);
|
|
185
|
+
var session = this._h2Pool[key];
|
|
186
|
+
if (!session) return null;
|
|
187
|
+
if (session.closed || session.destroyed) {
|
|
188
|
+
delete this._h2Pool[key];
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return session;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
setH2Session(host, port, session) {
|
|
195
|
+
var key = host + ':' + (port || 443);
|
|
196
|
+
this._h2Pool[key] = session;
|
|
197
|
+
session.on('close', () => {
|
|
198
|
+
if (this._h2Pool[key] === session) delete this._h2Pool[key];
|
|
199
|
+
});
|
|
200
|
+
session.on('error', () => {
|
|
201
|
+
if (this._h2Pool[key] === session) delete this._h2Pool[key];
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
// ============================================================
|
|
207
|
+
// Active request tracking
|
|
208
|
+
// ============================================================
|
|
209
|
+
|
|
210
|
+
canCreateSocket(host, port) {
|
|
211
|
+
var key = host + ':' + (port || 443);
|
|
212
|
+
return (this._activeCounts[key] || 0) < this.maxSockets;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
trackRequest(host, port) {
|
|
216
|
+
var key = host + ':' + (port || 443);
|
|
217
|
+
this._activeCounts[key] = (this._activeCounts[key] || 0) + 1;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
releaseRequest(host, port) {
|
|
221
|
+
var key = host + ':' + (port || 443);
|
|
222
|
+
if (this._activeCounts[key]) {
|
|
223
|
+
this._activeCounts[key]--;
|
|
224
|
+
if (this._activeCounts[key] <= 0) delete this._activeCounts[key];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
// ============================================================
|
|
230
|
+
// Sweep stale entries
|
|
231
|
+
// ============================================================
|
|
232
|
+
|
|
233
|
+
_sweep() {
|
|
234
|
+
var now = Date.now();
|
|
235
|
+
|
|
236
|
+
for (var key in this._protocolCache) {
|
|
237
|
+
var entry = this._protocolCache[key];
|
|
238
|
+
var maxAge = entry.altSvcMaxAge || 86400000;
|
|
239
|
+
if (now - entry.ts > maxAge) delete this._protocolCache[key];
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (var key in this._h3Pool) {
|
|
243
|
+
var conn = this._h3Pool[key];
|
|
244
|
+
var state = conn.quic ? conn.quic.state : 'closed';
|
|
245
|
+
if (state === 'closed' || state === 'draining') delete this._h3Pool[key];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
for (var key in this._h2Pool) {
|
|
249
|
+
var session = this._h2Pool[key];
|
|
250
|
+
if (session.closed || session.destroyed) delete this._h2Pool[key];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
destroy() {
|
|
255
|
+
if (this._sweepTimer) { clearInterval(this._sweepTimer); this._sweepTimer = null; }
|
|
256
|
+
|
|
257
|
+
for (var key in this._h3Pool) {
|
|
258
|
+
try { this._h3Pool[key].quic.close(0, 'agent destroy'); } catch (e) {}
|
|
259
|
+
}
|
|
260
|
+
this._h3Pool = {};
|
|
261
|
+
|
|
262
|
+
for (var key in this._h2Pool) {
|
|
263
|
+
try { this._h2Pool[key].close(); } catch (e) {}
|
|
264
|
+
}
|
|
265
|
+
this._h2Pool = {};
|
|
266
|
+
|
|
267
|
+
this._protocolCache = {};
|
|
268
|
+
this._activeCounts = {};
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
var globalAgent = new Agent();
|
|
274
|
+
|
|
275
|
+
export { Agent, globalAgent };
|