quico 0.2.0 → 0.4.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 +854 -125
- package/index.js +8 -2
- package/package.json +2 -2
- package/src/agent.js +67 -1
- package/src/client.js +40 -6
- package/src/crypto.js +37 -5
- package/src/h3.js +52 -43
- package/src/h3_client.js +103 -79
- package/src/h3_server.js +52 -183
- package/src/quic_connection.js +1397 -123
- package/src/quic_server.js +372 -0
- package/src/quic_socket.js +127 -0
- package/src/server.js +30 -22
- package/src/streams.js +15 -1
- package/src/tls_bridge.js +116 -14
- package/src/transport.js +143 -34
- package/src/utils.js +59 -4
- package/src/webtransport.js +25 -78
package/src/h3_client.js
CHANGED
|
@@ -12,10 +12,10 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import dgram from 'node:dgram';
|
|
16
15
|
import dns from 'node:dns';
|
|
17
|
-
import {
|
|
16
|
+
import { DEBUG } from './utils.js';
|
|
18
17
|
import { H3Connection } from './h3.js';
|
|
18
|
+
import { createQuicClientSocket } from './quic_socket.js';
|
|
19
19
|
|
|
20
20
|
|
|
21
21
|
// ============================================================
|
|
@@ -34,15 +34,27 @@ function request(options, callback) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
var hostname = options.hostname || options.host || 'localhost';
|
|
37
|
+
// Certificate verification options, forwarded to the QUIC/TLS layer.
|
|
38
|
+
// Defaults to verifying (node:tls semantics); without this passthrough the
|
|
39
|
+
// option dies here and H3 connections can never opt out — which makes the
|
|
40
|
+
// one transport this library exists for the only one that can't reach a
|
|
41
|
+
// self-signed or private-CA server (H2/H1.1 fallbacks honour the option).
|
|
42
|
+
var rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
43
|
+
var ca = options.ca || null;
|
|
37
44
|
var port = options.port || 443;
|
|
38
45
|
var path = options.path || '/';
|
|
39
46
|
var method = (options.method || 'GET').toUpperCase();
|
|
40
47
|
var headers = options.headers || {};
|
|
41
|
-
var rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
42
48
|
|
|
43
49
|
// Connection reuse: pass _connection = { quic, h3, udpSocket, nextStreamId }
|
|
44
50
|
var existingConn = options._connection || null;
|
|
45
|
-
|
|
51
|
+
// Auto-close the QUIC connection when this request finishes ONLY when we own
|
|
52
|
+
// it — i.e. a fresh, standalone connection. Reused connections (existingConn)
|
|
53
|
+
// and connections whose lifecycle is managed by an external pool
|
|
54
|
+
// (options._managed, set by the unified client/agent) are left open; closing
|
|
55
|
+
// them here would tear down a connection still pooled for reuse. Managed
|
|
56
|
+
// connections are reaped by the agent (sweep/destroy) and the QUIC idle timer.
|
|
57
|
+
var autoClose = !existingConn && !options._managed;
|
|
46
58
|
|
|
47
59
|
// ---- Client Request object ----
|
|
48
60
|
var reqBody = [];
|
|
@@ -104,9 +116,12 @@ function request(options, callback) {
|
|
|
104
116
|
|
|
105
117
|
// ---- Resolve hostname and connect (or reuse existing) ----
|
|
106
118
|
if (existingConn) {
|
|
107
|
-
// Reuse existing connection — allocate stream ID and send when ready
|
|
119
|
+
// Reuse existing connection — allocate stream ID and send when ready.
|
|
120
|
+
// The shared dispatch listeners were installed when the connection was
|
|
121
|
+
// first created; installSharedHandlers() is idempotent.
|
|
108
122
|
streamId = allocateStreamId(existingConn);
|
|
109
|
-
|
|
123
|
+
installSharedHandlers(existingConn);
|
|
124
|
+
setupResponseHandlers(existingConn);
|
|
110
125
|
if (reqFinished) sendRequest();
|
|
111
126
|
} else {
|
|
112
127
|
resolveAndConnect(hostname, port);
|
|
@@ -132,60 +147,42 @@ function request(options, callback) {
|
|
|
132
147
|
|
|
133
148
|
|
|
134
149
|
function startConnection(remoteIp, remotePort) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
});
|
|
150
|
+
udpSocket = createQuicClientSocket({
|
|
151
|
+
remoteIp: remoteIp,
|
|
152
|
+
remotePort: remotePort,
|
|
153
|
+
hostname: hostname,
|
|
154
|
+
rejectUnauthorized: rejectUnauthorized,
|
|
155
|
+
ca: ca,
|
|
156
|
+
|
|
157
|
+
onError: function (err) {
|
|
158
|
+
emit(reqListeners, 'error', err);
|
|
159
|
+
},
|
|
146
160
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
isServer: false,
|
|
152
|
-
SNICallback: null,
|
|
153
|
-
hostname: hostname
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Send packets via UDP
|
|
157
|
-
quic.on('packet', function (data) {
|
|
158
|
-
udpSocket.send(data, remotePort, remoteIp, function (err) {
|
|
159
|
-
if (err) emit(reqListeners, 'error', err);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// QUIC connected → set up H3
|
|
164
|
-
quic.on('connect', function () {
|
|
165
|
-
console.log('[client] QUIC connected');
|
|
161
|
+
onConnect: function (q, s) {
|
|
162
|
+
quic = q;
|
|
163
|
+
udpSocket = s;
|
|
164
|
+
if (DEBUG) console.log('[client] QUIC connected');
|
|
166
165
|
|
|
167
166
|
h3 = new H3Connection({ quicConnection: quic, isServer: false });
|
|
168
167
|
connected = true;
|
|
169
168
|
|
|
170
169
|
// Build connection object for reuse
|
|
171
170
|
var conn = { quic: quic, h3: h3, udpSocket: udpSocket, _nextStreamId: 0 };
|
|
171
|
+
|
|
172
|
+
// Install the shared per-connection dispatch listeners ONCE, then
|
|
173
|
+
// register this request in the connection's stream-handler map.
|
|
174
|
+
installSharedHandlers(conn);
|
|
172
175
|
streamId = allocateStreamId(conn);
|
|
173
176
|
|
|
174
177
|
// Store on clientReq so caller (unified client) can reuse
|
|
175
178
|
clientReq._connection = conn;
|
|
176
179
|
|
|
177
|
-
setupResponseHandlers();
|
|
180
|
+
setupResponseHandlers(conn);
|
|
178
181
|
|
|
179
182
|
// Send request if body is ready
|
|
180
183
|
if (reqFinished) sendRequest();
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
quic.on('close', function () {
|
|
184
|
-
if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
// Start QUIC handshake
|
|
188
|
-
quic.connect();
|
|
184
|
+
}
|
|
185
|
+
// onClose: the helper closes the UDP socket; nothing extra needed here.
|
|
189
186
|
});
|
|
190
187
|
}
|
|
191
188
|
|
|
@@ -202,51 +199,78 @@ function request(options, callback) {
|
|
|
202
199
|
|
|
203
200
|
|
|
204
201
|
/**
|
|
205
|
-
*
|
|
206
|
-
*
|
|
202
|
+
* Install the three HTTP/3 event listeners ONCE per connection. They dispatch
|
|
203
|
+
* each event to the right request via the connection's stream-handler map
|
|
204
|
+
* (keyed by stream id), so multiplexing many requests over a single
|
|
205
|
+
* connection does NOT pile up listeners on the H3 emitter (previously every
|
|
206
|
+
* request added another set of listeners that were never removed).
|
|
207
|
+
* Idempotent per connection.
|
|
207
208
|
*/
|
|
208
|
-
function
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
// Fire callback with response
|
|
217
|
-
if (typeof callback === 'function') {
|
|
218
|
-
callback(clientRes);
|
|
219
|
-
callback = null; // only once
|
|
220
|
-
}
|
|
221
|
-
emit(reqListeners, 'response', clientRes);
|
|
209
|
+
function installSharedHandlers(conn) {
|
|
210
|
+
if (conn._streamHandlers) return; // already installed for this connection
|
|
211
|
+
conn._streamHandlers = {};
|
|
212
|
+
var h3c = conn.h3;
|
|
213
|
+
|
|
214
|
+
h3c.on('http_headers', function (sid, hdrs) {
|
|
215
|
+
var h = conn._streamHandlers[sid];
|
|
216
|
+
if (h) h.onHeaders(hdrs);
|
|
222
217
|
});
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if (sid !== streamId) return;
|
|
227
|
-
emit(clientRes, 'data', data);
|
|
218
|
+
h3c.on('http_body', function (sid, data) {
|
|
219
|
+
var h = conn._streamHandlers[sid];
|
|
220
|
+
if (h) h.onBody(data);
|
|
228
221
|
});
|
|
222
|
+
h3c.on('http_end', function (sid) {
|
|
223
|
+
var h = conn._streamHandlers[sid];
|
|
224
|
+
if (!h) return;
|
|
225
|
+
delete conn._streamHandlers[sid]; // free per-stream state before dispatch
|
|
226
|
+
h.onEnd();
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
229
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Register this request's response handlers in the connection's stream map,
|
|
233
|
+
* keyed by its streamId. Replaces per-request h3.on(...) registration —
|
|
234
|
+
* routing is now by map key, so no per-listener streamId check is needed.
|
|
235
|
+
*/
|
|
236
|
+
function setupResponseHandlers(conn) {
|
|
237
|
+
conn._streamHandlers[streamId] = {
|
|
238
|
+
onHeaders: function (hdrs) {
|
|
239
|
+
clientRes.headers = hdrs;
|
|
240
|
+
clientRes.statusCode = parseInt(hdrs[':status']) || 0;
|
|
241
|
+
clientRes._headersReceived = true;
|
|
242
|
+
|
|
243
|
+
// Fire callback with response (once)
|
|
244
|
+
if (typeof callback === 'function') {
|
|
245
|
+
callback(clientRes);
|
|
246
|
+
callback = null;
|
|
247
|
+
}
|
|
248
|
+
emit(reqListeners, 'response', clientRes);
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
onBody: function (data) {
|
|
252
|
+
emit(clientRes, 'data', data);
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
onEnd: function () {
|
|
256
|
+
emit(clientRes, 'end');
|
|
257
|
+
|
|
258
|
+
// Auto-close only if we own the connection (see `autoClose` above).
|
|
259
|
+
if (autoClose) {
|
|
260
|
+
setTimeout(function () {
|
|
261
|
+
if (quic) quic.close(0, 'done');
|
|
262
|
+
if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
263
|
+
}, 100);
|
|
264
|
+
}
|
|
241
265
|
}
|
|
242
|
-
}
|
|
266
|
+
};
|
|
243
267
|
}
|
|
244
268
|
|
|
245
269
|
|
|
246
270
|
function sendRequest() {
|
|
247
271
|
if (!h3 || !connected) return;
|
|
248
272
|
|
|
249
|
-
console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
273
|
+
if (DEBUG) console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
250
274
|
|
|
251
275
|
// MUST send control streams (SETTINGS) before any request
|
|
252
276
|
h3.sendControlStreams();
|
package/src/h3_server.js
CHANGED
|
@@ -12,13 +12,10 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import dgram from 'node:dgram';
|
|
16
15
|
import { Duplex, Readable, Writable } from 'node:stream';
|
|
17
|
-
import { Emitter, readVarInt,
|
|
18
|
-
import {
|
|
19
|
-
import { QUICConnection } from './quic_connection.js';
|
|
16
|
+
import { Emitter, readVarInt, wt_datagram_payload } from './utils.js';
|
|
17
|
+
import { createQuicServer } from './quic_server.js';
|
|
20
18
|
import { H3Connection } from './h3.js';
|
|
21
|
-
import { createSecureContext } from './tls_bridge.js';
|
|
22
19
|
|
|
23
20
|
|
|
24
21
|
// ============================================================
|
|
@@ -29,23 +26,26 @@ function createServer(options, handler) {
|
|
|
29
26
|
options = options || {};
|
|
30
27
|
|
|
31
28
|
var ev = Emitter();
|
|
32
|
-
var udp4 = null;
|
|
33
|
-
var udp6 = null;
|
|
34
|
-
var port = null;
|
|
35
29
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
30
|
+
// Transport-only QUIC server owns the UDP socket(s), demux, connection
|
|
31
|
+
// map and lifecycle. h3_server layers HTTP/3 on top by listening for its
|
|
32
|
+
// 'connection' event (see below) and wrapping each raw QUICConnection in
|
|
33
|
+
// an H3Connection. ALPN is 'h3'. External-socket (shared-port) mode and
|
|
34
|
+
// the handlePacket/hasConnection demux hooks are provided by the
|
|
35
|
+
// transport server and re-exported on this server's API.
|
|
36
|
+
var qserver = createQuicServer({
|
|
37
|
+
alpn: 'h3',
|
|
38
|
+
SNICallback: options.SNICallback || null,
|
|
39
|
+
key: options.key,
|
|
40
|
+
cert: options.cert,
|
|
41
|
+
ca: options.ca,
|
|
42
|
+
maxConnections: options.maxConnections || 10000,
|
|
43
|
+
socket: options.socket || null,
|
|
44
|
+
socket6: options.socket6 || null
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Surface transport-level (UDP socket / send) errors on this server.
|
|
48
|
+
qserver.on('error', function (err) { ev.emit('error', err); });
|
|
49
49
|
|
|
50
50
|
|
|
51
51
|
// ============================================================
|
|
@@ -105,8 +105,7 @@ function createServer(options, handler) {
|
|
|
105
105
|
// Same pattern as old quico h3_server.js
|
|
106
106
|
// ============================================================
|
|
107
107
|
|
|
108
|
-
function set_http_stream(
|
|
109
|
-
var conn = connections[connId];
|
|
108
|
+
function set_http_stream(conn, streamId, params) {
|
|
110
109
|
if (!conn) return;
|
|
111
110
|
|
|
112
111
|
var is_new = false;
|
|
@@ -200,12 +199,7 @@ function createServer(options, handler) {
|
|
|
200
199
|
if (!req._isWebTransport) return;
|
|
201
200
|
if (typeof data === 'string') data = Buffer.from(data);
|
|
202
201
|
if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
203
|
-
|
|
204
|
-
var prefix = writeVarInt(quarterSid);
|
|
205
|
-
var frame = new Uint8Array(prefix.length + data.byteLength);
|
|
206
|
-
frame.set(prefix, 0);
|
|
207
|
-
frame.set(data, prefix.length);
|
|
208
|
-
conn.quic.sendDatagram(frame);
|
|
202
|
+
conn.quic.sendDatagram(wt_datagram_payload(Number(streamId), data));
|
|
209
203
|
},
|
|
210
204
|
|
|
211
205
|
createBidirectionalStream: function () {
|
|
@@ -284,93 +278,41 @@ function createServer(options, handler) {
|
|
|
284
278
|
|
|
285
279
|
|
|
286
280
|
// ============================================================
|
|
287
|
-
//
|
|
281
|
+
// HTTP/3 over each accepted QUIC connection
|
|
282
|
+
// The transport server hands us a raw QUICConnection per peer; we wrap
|
|
283
|
+
// it in an H3Connection once the handshake completes and bridge HTTP/3 +
|
|
284
|
+
// WebTransport events to req/res via set_http_stream. Per-connection
|
|
285
|
+
// state lives in this closure — the transport server owns the UDP
|
|
286
|
+
// socket, demux and connection lifecycle.
|
|
288
287
|
// ============================================================
|
|
289
288
|
|
|
290
|
-
function
|
|
291
|
-
var
|
|
292
|
-
var fromIp = rinfo.address;
|
|
293
|
-
var fromPort = rinfo.port;
|
|
294
|
-
console.log('[server] UDP from ' + fromIp + ':' + fromPort + ' len=' + data.length);
|
|
295
|
-
|
|
296
|
-
var addressKey = fromIp + ':' + fromPort;
|
|
297
|
-
var packets = parse_quic_datagram(data);
|
|
298
|
-
if (packets.length === 0) return;
|
|
299
|
-
|
|
300
|
-
for (var i = 0; i < packets.length; i++) {
|
|
301
|
-
var pkt = packets[i];
|
|
302
|
-
if (!pkt) continue;
|
|
303
|
-
|
|
304
|
-
var connId = findConnectionId(pkt, addressKey);
|
|
305
|
-
|
|
306
|
-
if (!(connId in connections)) {
|
|
307
|
-
// Reject new connections when at capacity
|
|
308
|
-
if (Object.keys(connections).length >= maxConnections) return;
|
|
309
|
-
createConnection(connId, fromIp, fromPort, addressKey);
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
connections[connId].quic.feedDatagram(fromIp, fromPort, pkt.raw);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
function findConnectionId(pkt, addressKey) {
|
|
318
|
-
var dcidStr = null;
|
|
319
|
-
if (pkt.dcid && pkt.dcid.byteLength > 0) {
|
|
320
|
-
dcidStr = Array.from(pkt.dcid).join('');
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (dcidStr && dcidStr in connections) return dcidStr;
|
|
324
|
-
if (addressKey in addressMap && addressMap[addressKey] in connections) return addressMap[addressKey];
|
|
325
|
-
|
|
326
|
-
var id = dcidStr || String(Math.floor(Math.random() * 9007199254740991));
|
|
327
|
-
addressMap[addressKey] = id;
|
|
328
|
-
return id;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
function createConnection(connId, fromIp, fromPort, addressKey) {
|
|
333
|
-
var quic = new QUICConnection({ SNICallback: sniCallback });
|
|
334
|
-
|
|
335
|
-
connections[connId] = {
|
|
336
|
-
quic: quic,
|
|
337
|
-
h3: null,
|
|
338
|
-
http_streams: {}
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
addressMap[addressKey] = connId;
|
|
342
|
-
|
|
343
|
-
// Send packets back via UDP
|
|
344
|
-
quic.on('packet', function (data) {
|
|
345
|
-
sendUdp(fromIp, fromPort, data);
|
|
346
|
-
});
|
|
289
|
+
qserver.on('connection', function (quic, peer) {
|
|
290
|
+
var conn = { quic: quic, h3: null, http_streams: {} };
|
|
347
291
|
|
|
348
292
|
// QUIC handshake complete → set up H3
|
|
349
293
|
quic.on('connect', function () {
|
|
350
294
|
var h3 = new H3Connection({ quicConnection: quic, enableWebTransport: true });
|
|
351
|
-
|
|
295
|
+
conn.h3 = h3;
|
|
352
296
|
|
|
353
297
|
// HTTP request headers received → set_http_stream (reactive)
|
|
354
298
|
h3.on('http_headers', function (streamId, headers) {
|
|
355
|
-
set_http_stream(
|
|
299
|
+
set_http_stream(conn, streamId, { request_headers: headers });
|
|
356
300
|
});
|
|
357
301
|
|
|
358
302
|
// HTTP request body chunk → set_http_stream
|
|
359
303
|
h3.on('http_body', function (streamId, data) {
|
|
360
|
-
set_http_stream(
|
|
304
|
+
set_http_stream(conn, streamId, { request_body_chunk: data });
|
|
361
305
|
});
|
|
362
306
|
|
|
363
307
|
// HTTP request complete → fire req.on('end')
|
|
364
308
|
h3.on('http_end', function (streamId) {
|
|
365
|
-
set_http_stream(
|
|
309
|
+
set_http_stream(conn, streamId, { request_end: true });
|
|
366
310
|
});
|
|
367
311
|
|
|
368
312
|
// ---- WebTransport stream events ----
|
|
369
313
|
|
|
370
314
|
// New WT stream (bidi or uni from client)
|
|
371
315
|
h3.on('wt_stream', function (sessionId, wtStreamId, data, fin, isBidi) {
|
|
372
|
-
var conn = connections[connId];
|
|
373
|
-
if (!conn) return;
|
|
374
316
|
var session = conn.http_streams[sessionId];
|
|
375
317
|
if (!session || !session.req._isWebTransport) return;
|
|
376
318
|
|
|
@@ -395,8 +337,7 @@ function createServer(options, handler) {
|
|
|
395
337
|
|
|
396
338
|
// Subsequent data on WT stream
|
|
397
339
|
h3.on('wt_data', function (sessionId, wtStreamId, data, fin) {
|
|
398
|
-
|
|
399
|
-
if (!conn || !conn._wtStreamObjects) return;
|
|
340
|
+
if (!conn._wtStreamObjects) return;
|
|
400
341
|
var wtStream = conn._wtStreamObjects[wtStreamId];
|
|
401
342
|
if (!wtStream) return;
|
|
402
343
|
if (data.byteLength > 0) wtStream._pushData(data);
|
|
@@ -404,7 +345,7 @@ function createServer(options, handler) {
|
|
|
404
345
|
});
|
|
405
346
|
|
|
406
347
|
// Datagrams
|
|
407
|
-
quic.on('datagram', function (
|
|
348
|
+
quic.on('datagram', function (rawData) {
|
|
408
349
|
if (!rawData || rawData.byteLength === 0) return;
|
|
409
350
|
var result = readVarInt(rawData, 0);
|
|
410
351
|
if (!result) return;
|
|
@@ -412,8 +353,6 @@ function createServer(options, handler) {
|
|
|
412
353
|
var payload = rawData.slice(result.byteLength);
|
|
413
354
|
|
|
414
355
|
// Find the session: quarter_stream_id = session_stream_id / 4
|
|
415
|
-
var conn = connections[connId];
|
|
416
|
-
if (!conn) return;
|
|
417
356
|
for (var sid in conn.http_streams) {
|
|
418
357
|
var session = conn.http_streams[sid];
|
|
419
358
|
if (session.req._isWebTransport && Math.floor(Number(sid) / 4) === quarterSid) {
|
|
@@ -423,99 +362,29 @@ function createServer(options, handler) {
|
|
|
423
362
|
}
|
|
424
363
|
});
|
|
425
364
|
});
|
|
426
|
-
|
|
427
|
-
quic.on('close', function () {
|
|
428
|
-
delete connections[connId];
|
|
429
|
-
if (addressMap[addressKey] === connId) delete addressMap[addressKey];
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
// ============================================================
|
|
435
|
-
// UDP send
|
|
436
|
-
// ============================================================
|
|
437
|
-
|
|
438
|
-
function sendUdp(toIp, toPort, data) {
|
|
439
|
-
var socket = toIp.indexOf(':') >= 0 ? udp6 : udp4;
|
|
440
|
-
if (!socket) return;
|
|
441
|
-
socket.send(data, toPort, toIp, function (err) {
|
|
442
|
-
if (err) ev.emit('error', err);
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
// ============================================================
|
|
448
|
-
// listen / close
|
|
449
|
-
// ============================================================
|
|
450
|
-
|
|
451
|
-
function listen(listenPort, host, callback) {
|
|
452
|
-
if (typeof host === 'function') { callback = host; host = null; }
|
|
453
|
-
port = listenPort || 443;
|
|
454
|
-
host = host || '::';
|
|
455
|
-
|
|
456
|
-
udp4 = dgram.createSocket('udp4');
|
|
457
|
-
udp4.on('message', onUdpMessage);
|
|
458
|
-
udp4.on('error', function (err) { ev.emit('error', err); });
|
|
459
|
-
var host4 = host.indexOf('.') !== -1 ? host : '0.0.0.0';
|
|
460
|
-
udp4.bind(port, host4);
|
|
461
|
-
|
|
462
|
-
udp6 = dgram.createSocket({ type: 'udp6', ipv6Only: true });
|
|
463
|
-
udp6.on('message', onUdpMessage);
|
|
464
|
-
udp6.on('error', function (err) { ev.emit('error', err); });
|
|
465
|
-
var host6 = host.indexOf(':') !== -1 ? host : '::';
|
|
466
|
-
udp6.bind(port, host6, function () {
|
|
467
|
-
if (typeof callback === 'function') callback();
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// Periodic sweep for dead connections (safety net)
|
|
471
|
-
sweepTimer = setInterval(function () {
|
|
472
|
-
for (var id in connections) {
|
|
473
|
-
var st = connections[id].quic.state;
|
|
474
|
-
if (st === 'closed') {
|
|
475
|
-
delete connections[id];
|
|
476
|
-
// Clean address map
|
|
477
|
-
for (var addr in addressMap) {
|
|
478
|
-
if (addressMap[addr] === id) delete addressMap[addr];
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
}, 30000);
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
function close(callback) {
|
|
486
|
-
// Stop sweep
|
|
487
|
-
if (sweepTimer) { clearInterval(sweepTimer); sweepTimer = null; }
|
|
488
|
-
|
|
489
|
-
// Close all connections
|
|
490
|
-
for (var id in connections) {
|
|
491
|
-
try { connections[id].quic.close(0, 'server closing'); } catch (e) { }
|
|
492
|
-
}
|
|
493
|
-
connections = {};
|
|
494
|
-
addressMap = {};
|
|
495
|
-
|
|
496
|
-
var closed = 0;
|
|
497
|
-
var total = (udp4 ? 1 : 0) + (udp6 ? 1 : 0);
|
|
498
|
-
if (total === 0) { if (callback) callback(); return; }
|
|
499
|
-
|
|
500
|
-
function onClose() {
|
|
501
|
-
closed++;
|
|
502
|
-
if (closed >= total && typeof callback === 'function') callback();
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
if (udp4) { udp4.close(onClose); udp4 = null; }
|
|
506
|
-
if (udp6) { udp6.close(onClose); udp6 = null; }
|
|
507
|
-
}
|
|
365
|
+
});
|
|
508
366
|
|
|
509
367
|
|
|
510
368
|
// ============================================================
|
|
511
369
|
// Server API
|
|
370
|
+
// Transport concerns (UDP sockets, demux, connection lifecycle,
|
|
371
|
+
// shared-port handlePacket/hasConnection) are delegated to the QUIC
|
|
372
|
+
// server. This layer adds HTTP/3 request events on top.
|
|
512
373
|
// ============================================================
|
|
513
374
|
|
|
514
375
|
return {
|
|
515
|
-
listen: listen,
|
|
516
|
-
close: close,
|
|
376
|
+
listen: qserver.listen,
|
|
377
|
+
close: qserver.close,
|
|
517
378
|
on: function (name, fn) { ev.on(name, fn); },
|
|
518
|
-
setTimeout: function () { /* TODO */ }
|
|
379
|
+
setTimeout: function () { /* TODO */ },
|
|
380
|
+
|
|
381
|
+
/** Feed an incoming UDP packet when running in external-socket mode.
|
|
382
|
+
* rinfo must be in Node's dgram format: { address, port, family, size }. */
|
|
383
|
+
handlePacket: qserver.handlePacket,
|
|
384
|
+
|
|
385
|
+
/** Returns true if the 5-tuple matches an active QUIC connection.
|
|
386
|
+
* Used by demuxers for routing decisions on shared UDP ports. */
|
|
387
|
+
hasConnection: qserver.hasConnection
|
|
519
388
|
};
|
|
520
389
|
}
|
|
521
390
|
|