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/h3_client.js
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
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 dns from 'node:dns';
|
|
16
|
+
import { DEBUG } from './utils.js';
|
|
17
|
+
import { H3Connection } from './h3.js';
|
|
18
|
+
import { createQuicClientSocket } from './quic_socket.js';
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
// ============================================================
|
|
22
|
+
// quico.request(options, callback)
|
|
23
|
+
// API mirrors Node.js https.request()
|
|
24
|
+
// ============================================================
|
|
25
|
+
|
|
26
|
+
function request(options, callback) {
|
|
27
|
+
if (typeof options === 'string') {
|
|
28
|
+
var url = new URL(options);
|
|
29
|
+
options = {
|
|
30
|
+
hostname: url.hostname,
|
|
31
|
+
port: parseInt(url.port) || 443,
|
|
32
|
+
path: url.pathname + url.search
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
var hostname = options.hostname || options.host || 'localhost';
|
|
37
|
+
var port = options.port || 443;
|
|
38
|
+
var path = options.path || '/';
|
|
39
|
+
var method = (options.method || 'GET').toUpperCase();
|
|
40
|
+
var headers = options.headers || {};
|
|
41
|
+
|
|
42
|
+
// Connection reuse: pass _connection = { quic, h3, udpSocket, nextStreamId }
|
|
43
|
+
var existingConn = options._connection || null;
|
|
44
|
+
// Auto-close the QUIC connection when this request finishes ONLY when we own
|
|
45
|
+
// it — i.e. a fresh, standalone connection. Reused connections (existingConn)
|
|
46
|
+
// and connections whose lifecycle is managed by an external pool
|
|
47
|
+
// (options._managed, set by the unified client/agent) are left open; closing
|
|
48
|
+
// them here would tear down a connection still pooled for reuse. Managed
|
|
49
|
+
// connections are reaped by the agent (sweep/destroy) and the QUIC idle timer.
|
|
50
|
+
var autoClose = !existingConn && !options._managed;
|
|
51
|
+
|
|
52
|
+
// ---- Client Request object ----
|
|
53
|
+
var reqBody = [];
|
|
54
|
+
var reqFinished = false;
|
|
55
|
+
var reqListeners = {};
|
|
56
|
+
var quic = existingConn ? existingConn.quic : null;
|
|
57
|
+
var h3 = existingConn ? existingConn.h3 : null;
|
|
58
|
+
var udpSocket = existingConn ? existingConn.udpSocket : null;
|
|
59
|
+
var streamId = null; // assigned when connection is ready
|
|
60
|
+
var connected = existingConn ? true : false;
|
|
61
|
+
|
|
62
|
+
var clientReq = {
|
|
63
|
+
on: function (event, fn) {
|
|
64
|
+
(reqListeners[event] = reqListeners[event] || []).push(fn);
|
|
65
|
+
return clientReq;
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
write: function (chunk) {
|
|
69
|
+
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
70
|
+
reqBody.push(chunk);
|
|
71
|
+
return true;
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
end: function (chunk) {
|
|
75
|
+
if (chunk) clientReq.write(chunk);
|
|
76
|
+
reqFinished = true;
|
|
77
|
+
if (connected) sendRequest();
|
|
78
|
+
return clientReq;
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
destroy: function () {
|
|
82
|
+
if (quic) quic.close(0, 'client destroy');
|
|
83
|
+
if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ---- Client Response object ----
|
|
88
|
+
var clientRes = {
|
|
89
|
+
statusCode: 0,
|
|
90
|
+
headers: {},
|
|
91
|
+
_listeners: {},
|
|
92
|
+
_headersReceived: false,
|
|
93
|
+
|
|
94
|
+
on: function (event, fn) {
|
|
95
|
+
(clientRes._listeners[event] = clientRes._listeners[event] || []).push(fn);
|
|
96
|
+
return clientRes;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
function emit(obj, event, data) {
|
|
101
|
+
var listeners = (obj._listeners || obj)[event] || [];
|
|
102
|
+
// For reqListeners which is a plain object
|
|
103
|
+
if (obj === reqListeners) listeners = reqListeners[event] || [];
|
|
104
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
105
|
+
try { if (data !== undefined) listeners[i](data); else listeners[i](); } catch (e) {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
// ---- Resolve hostname and connect (or reuse existing) ----
|
|
111
|
+
if (existingConn) {
|
|
112
|
+
// Reuse existing connection — allocate stream ID and send when ready.
|
|
113
|
+
// The shared dispatch listeners were installed when the connection was
|
|
114
|
+
// first created; installSharedHandlers() is idempotent.
|
|
115
|
+
streamId = allocateStreamId(existingConn);
|
|
116
|
+
installSharedHandlers(existingConn);
|
|
117
|
+
setupResponseHandlers(existingConn);
|
|
118
|
+
if (reqFinished) sendRequest();
|
|
119
|
+
} else {
|
|
120
|
+
resolveAndConnect(hostname, port);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
function resolveAndConnect(host, port) {
|
|
125
|
+
// If already IP, skip DNS
|
|
126
|
+
if (/^[\d.]+$/.test(host) || host.indexOf(':') >= 0 || host === 'localhost') {
|
|
127
|
+
var ip = (host === 'localhost') ? '127.0.0.1' : host;
|
|
128
|
+
startConnection(ip, port);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
dns.lookup(host, function (err, address) {
|
|
133
|
+
if (err) {
|
|
134
|
+
emit(reqListeners, 'error', err);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
startConnection(address, port);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
function startConnection(remoteIp, remotePort) {
|
|
143
|
+
udpSocket = createQuicClientSocket({
|
|
144
|
+
remoteIp: remoteIp,
|
|
145
|
+
remotePort: remotePort,
|
|
146
|
+
hostname: hostname,
|
|
147
|
+
|
|
148
|
+
onError: function (err) {
|
|
149
|
+
emit(reqListeners, 'error', err);
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
onConnect: function (q, s) {
|
|
153
|
+
quic = q;
|
|
154
|
+
udpSocket = s;
|
|
155
|
+
if (DEBUG) console.log('[client] QUIC connected');
|
|
156
|
+
|
|
157
|
+
h3 = new H3Connection({ quicConnection: quic, isServer: false });
|
|
158
|
+
connected = true;
|
|
159
|
+
|
|
160
|
+
// Build connection object for reuse
|
|
161
|
+
var conn = { quic: quic, h3: h3, udpSocket: udpSocket, _nextStreamId: 0 };
|
|
162
|
+
|
|
163
|
+
// Install the shared per-connection dispatch listeners ONCE, then
|
|
164
|
+
// register this request in the connection's stream-handler map.
|
|
165
|
+
installSharedHandlers(conn);
|
|
166
|
+
streamId = allocateStreamId(conn);
|
|
167
|
+
|
|
168
|
+
// Store on clientReq so caller (unified client) can reuse
|
|
169
|
+
clientReq._connection = conn;
|
|
170
|
+
|
|
171
|
+
setupResponseHandlers(conn);
|
|
172
|
+
|
|
173
|
+
// Send request if body is ready
|
|
174
|
+
if (reqFinished) sendRequest();
|
|
175
|
+
}
|
|
176
|
+
// onClose: the helper closes the UDP socket; nothing extra needed here.
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Allocate next client-initiated bidi stream ID.
|
|
183
|
+
* Client bidi stream IDs: 0, 4, 8, 12, ... (id = n * 4)
|
|
184
|
+
*/
|
|
185
|
+
function allocateStreamId(conn) {
|
|
186
|
+
var id = conn._nextStreamId;
|
|
187
|
+
conn._nextStreamId += 4;
|
|
188
|
+
return id;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Install the three HTTP/3 event listeners ONCE per connection. They dispatch
|
|
194
|
+
* each event to the right request via the connection's stream-handler map
|
|
195
|
+
* (keyed by stream id), so multiplexing many requests over a single
|
|
196
|
+
* connection does NOT pile up listeners on the H3 emitter (previously every
|
|
197
|
+
* request added another set of listeners that were never removed).
|
|
198
|
+
* Idempotent per connection.
|
|
199
|
+
*/
|
|
200
|
+
function installSharedHandlers(conn) {
|
|
201
|
+
if (conn._streamHandlers) return; // already installed for this connection
|
|
202
|
+
conn._streamHandlers = {};
|
|
203
|
+
var h3c = conn.h3;
|
|
204
|
+
|
|
205
|
+
h3c.on('http_headers', function (sid, hdrs) {
|
|
206
|
+
var h = conn._streamHandlers[sid];
|
|
207
|
+
if (h) h.onHeaders(hdrs);
|
|
208
|
+
});
|
|
209
|
+
h3c.on('http_body', function (sid, data) {
|
|
210
|
+
var h = conn._streamHandlers[sid];
|
|
211
|
+
if (h) h.onBody(data);
|
|
212
|
+
});
|
|
213
|
+
h3c.on('http_end', function (sid) {
|
|
214
|
+
var h = conn._streamHandlers[sid];
|
|
215
|
+
if (!h) return;
|
|
216
|
+
delete conn._streamHandlers[sid]; // free per-stream state before dispatch
|
|
217
|
+
h.onEnd();
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Register this request's response handlers in the connection's stream map,
|
|
224
|
+
* keyed by its streamId. Replaces per-request h3.on(...) registration —
|
|
225
|
+
* routing is now by map key, so no per-listener streamId check is needed.
|
|
226
|
+
*/
|
|
227
|
+
function setupResponseHandlers(conn) {
|
|
228
|
+
conn._streamHandlers[streamId] = {
|
|
229
|
+
onHeaders: function (hdrs) {
|
|
230
|
+
clientRes.headers = hdrs;
|
|
231
|
+
clientRes.statusCode = parseInt(hdrs[':status']) || 0;
|
|
232
|
+
clientRes._headersReceived = true;
|
|
233
|
+
|
|
234
|
+
// Fire callback with response (once)
|
|
235
|
+
if (typeof callback === 'function') {
|
|
236
|
+
callback(clientRes);
|
|
237
|
+
callback = null;
|
|
238
|
+
}
|
|
239
|
+
emit(reqListeners, 'response', clientRes);
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
onBody: function (data) {
|
|
243
|
+
emit(clientRes, 'data', data);
|
|
244
|
+
},
|
|
245
|
+
|
|
246
|
+
onEnd: function () {
|
|
247
|
+
emit(clientRes, 'end');
|
|
248
|
+
|
|
249
|
+
// Auto-close only if we own the connection (see `autoClose` above).
|
|
250
|
+
if (autoClose) {
|
|
251
|
+
setTimeout(function () {
|
|
252
|
+
if (quic) quic.close(0, 'done');
|
|
253
|
+
if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
254
|
+
}, 100);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
function sendRequest() {
|
|
262
|
+
if (!h3 || !connected) return;
|
|
263
|
+
|
|
264
|
+
if (DEBUG) console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
265
|
+
|
|
266
|
+
// MUST send control streams (SETTINGS) before any request
|
|
267
|
+
h3.sendControlStreams();
|
|
268
|
+
|
|
269
|
+
// Build H3 request headers
|
|
270
|
+
var h3Headers = {
|
|
271
|
+
':method': method,
|
|
272
|
+
':scheme': 'https',
|
|
273
|
+
':authority': hostname + (port !== 443 ? ':' + port : ''),
|
|
274
|
+
':path': path
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Add user headers
|
|
278
|
+
for (var name in headers) {
|
|
279
|
+
h3Headers[name.toLowerCase()] = String(headers[name]);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Default headers
|
|
283
|
+
if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
|
|
284
|
+
if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
|
|
285
|
+
if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
|
|
286
|
+
|
|
287
|
+
// Send body if any
|
|
288
|
+
if (reqBody.length > 0) {
|
|
289
|
+
// Send headers without FIN, then body chunks
|
|
290
|
+
h3.sendHeaders(streamId, h3Headers, false);
|
|
291
|
+
for (var i = 0; i < reqBody.length; i++) {
|
|
292
|
+
var isLast = (i === reqBody.length - 1);
|
|
293
|
+
h3.sendBody(streamId, reqBody[i], isLast);
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// No body — send headers with FIN directly
|
|
297
|
+
h3.sendHeaders(streamId, h3Headers, true);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
return clientReq;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
// ============================================================
|
|
307
|
+
// quico.get(url, callback)
|
|
308
|
+
// Shortcut — like https.get()
|
|
309
|
+
// ============================================================
|
|
310
|
+
|
|
311
|
+
function get(url, callback) {
|
|
312
|
+
var opts;
|
|
313
|
+
if (typeof url === 'string') {
|
|
314
|
+
var parsed = new URL(url);
|
|
315
|
+
opts = {
|
|
316
|
+
hostname: parsed.hostname,
|
|
317
|
+
port: parseInt(parsed.port) || 443,
|
|
318
|
+
path: parsed.pathname + parsed.search,
|
|
319
|
+
method: 'GET'
|
|
320
|
+
};
|
|
321
|
+
} else {
|
|
322
|
+
opts = url;
|
|
323
|
+
opts.method = 'GET';
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
var req = request(opts, callback);
|
|
327
|
+
req.end();
|
|
328
|
+
return req;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
export { request, get };
|