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/h3_server.js
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
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 dgram from 'node:dgram';
|
|
16
|
+
import { Duplex, Readable, Writable } from 'node:stream';
|
|
17
|
+
import { Emitter, readVarInt, writeVarInt } from './utils.js';
|
|
18
|
+
import { parse_quic_datagram } from './transport.js';
|
|
19
|
+
import { QUICConnection } from './quic_connection.js';
|
|
20
|
+
import { H3Connection } from './h3.js';
|
|
21
|
+
import { createSecureContext } from './tls_bridge.js';
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// ============================================================
|
|
25
|
+
// createServer(options, handler)
|
|
26
|
+
// ============================================================
|
|
27
|
+
|
|
28
|
+
function createServer(options, handler) {
|
|
29
|
+
options = options || {};
|
|
30
|
+
|
|
31
|
+
var ev = Emitter();
|
|
32
|
+
var udp4 = null;
|
|
33
|
+
var udp6 = null;
|
|
34
|
+
var port = null;
|
|
35
|
+
|
|
36
|
+
// Build SNICallback
|
|
37
|
+
var sniCallback = options.SNICallback || null;
|
|
38
|
+
|
|
39
|
+
if (!sniCallback && options.key && options.cert) {
|
|
40
|
+
var defaultCtx = createSecureContext({ key: options.key, cert: options.cert, ca: options.ca });
|
|
41
|
+
sniCallback = function (servername, cb) { cb(null, defaultCtx); };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---- Connection tracking ----
|
|
45
|
+
var connections = {}; // connId → { quic, h3, http_streams }
|
|
46
|
+
var addressMap = {}; // "ip:port" → connId
|
|
47
|
+
var maxConnections = options.maxConnections || 10000;
|
|
48
|
+
var sweepTimer = null;
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// ============================================================
|
|
52
|
+
// WebTransport helpers
|
|
53
|
+
// ============================================================
|
|
54
|
+
|
|
55
|
+
function _emitOnReq(req, event, data) {
|
|
56
|
+
// If unified server wrapped the req, emit on the wrapper (IncomingMessage/EventEmitter)
|
|
57
|
+
if (req._wrapper && typeof req._wrapper.emit === 'function') {
|
|
58
|
+
req._wrapper.emit(event, data);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
// Fallback: emit on raw req's _listeners
|
|
62
|
+
var listeners = req._listeners[event] || [];
|
|
63
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
64
|
+
try { if (data !== undefined) listeners[i](data); else listeners[i](); } catch (e) {}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function _createWtStream(conn, quic, wtStreamId, sessionId, isBidi) {
|
|
69
|
+
if (!conn._wtStreamObjects) conn._wtStreamObjects = {};
|
|
70
|
+
|
|
71
|
+
var stream;
|
|
72
|
+
if (isBidi) {
|
|
73
|
+
stream = new Duplex({
|
|
74
|
+
read: function () {},
|
|
75
|
+
write: function (chunk, encoding, cb) {
|
|
76
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
|
|
77
|
+
quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
|
|
78
|
+
cb();
|
|
79
|
+
},
|
|
80
|
+
final: function (cb) {
|
|
81
|
+
quic.sendStream(wtStreamId, new Uint8Array(0), true);
|
|
82
|
+
cb();
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
} else {
|
|
86
|
+
stream = new Readable({ read: function () {} });
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
stream._streamId = wtStreamId;
|
|
90
|
+
stream._sessionId = sessionId;
|
|
91
|
+
stream._pushData = function (data) {
|
|
92
|
+
if (data instanceof Uint8Array) data = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
|
|
93
|
+
stream.push(data);
|
|
94
|
+
};
|
|
95
|
+
stream._endData = function () { stream.push(null); };
|
|
96
|
+
|
|
97
|
+
conn._wtStreamObjects[wtStreamId] = stream;
|
|
98
|
+
return stream;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
// ============================================================
|
|
103
|
+
// set_http_stream — reactive HTTP stream builder
|
|
104
|
+
// Creates req/res on first call, fires handler when ready.
|
|
105
|
+
// Same pattern as old quico h3_server.js
|
|
106
|
+
// ============================================================
|
|
107
|
+
|
|
108
|
+
function set_http_stream(connId, streamId, params) {
|
|
109
|
+
var conn = connections[connId];
|
|
110
|
+
if (!conn) return;
|
|
111
|
+
|
|
112
|
+
var is_new = false;
|
|
113
|
+
|
|
114
|
+
// Create req/res on first encounter of this stream
|
|
115
|
+
if (!(streamId in conn.http_streams)) {
|
|
116
|
+
|
|
117
|
+
var req = {
|
|
118
|
+
method: null,
|
|
119
|
+
url: null,
|
|
120
|
+
headers: {},
|
|
121
|
+
httpVersion: '3.0',
|
|
122
|
+
stream_id: Number(streamId),
|
|
123
|
+
_listeners: {},
|
|
124
|
+
_isWebTransport: false,
|
|
125
|
+
|
|
126
|
+
on: function (event, fn) {
|
|
127
|
+
(req._listeners[event] = req._listeners[event] || []).push(fn);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
var res = {
|
|
132
|
+
statusCode: 200,
|
|
133
|
+
headers: {},
|
|
134
|
+
headersSent: false,
|
|
135
|
+
_finished: false,
|
|
136
|
+
|
|
137
|
+
setHeader: function (name, value) {
|
|
138
|
+
res.headers[name.toLowerCase()] = String(value);
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
getHeader: function (name) {
|
|
142
|
+
return res.headers[name.toLowerCase()];
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
writeHead: function (statusCode, headers) {
|
|
146
|
+
if (res.headersSent) return res;
|
|
147
|
+
res.statusCode = statusCode;
|
|
148
|
+
|
|
149
|
+
if (typeof headers === 'object') {
|
|
150
|
+
for (var name in headers) {
|
|
151
|
+
res.headers[name.toLowerCase()] = String(headers[name]);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
var h3Headers = { ':status': String(statusCode) };
|
|
156
|
+
for (var name in res.headers) {
|
|
157
|
+
h3Headers[name] = res.headers[name];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (conn.h3) {
|
|
161
|
+
// Send headers WITHOUT FIN for WT (session stays open)
|
|
162
|
+
var isWt = req.headers[':protocol'] === 'webtransport';
|
|
163
|
+
conn.h3.sendHeaders(streamId, h3Headers, isWt ? false : undefined);
|
|
164
|
+
}
|
|
165
|
+
res.headersSent = true;
|
|
166
|
+
|
|
167
|
+
// If this is a WT CONNECT with 200 → register session
|
|
168
|
+
if (req.headers[':protocol'] === 'webtransport' && statusCode === 200 && conn.h3) {
|
|
169
|
+
req._isWebTransport = true;
|
|
170
|
+
conn.h3.registerWebTransportSession(Number(streamId));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return res;
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
write: function (chunk) {
|
|
177
|
+
if (!res.headersSent) res.writeHead(res.statusCode);
|
|
178
|
+
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
179
|
+
if (conn.h3) conn.h3.sendBody(streamId, chunk, false);
|
|
180
|
+
return true;
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
end: function (chunk) {
|
|
184
|
+
if (res._finished) return;
|
|
185
|
+
if (!res.headersSent) res.writeHead(res.statusCode);
|
|
186
|
+
if (conn.h3) {
|
|
187
|
+
if (chunk) {
|
|
188
|
+
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
189
|
+
conn.h3.sendBody(streamId, chunk, true);
|
|
190
|
+
} else {
|
|
191
|
+
conn.h3.sendBody(streamId, null, true);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
res._finished = true;
|
|
195
|
+
},
|
|
196
|
+
|
|
197
|
+
// ---- WebTransport methods on res ----
|
|
198
|
+
|
|
199
|
+
sendDatagram: function (data) {
|
|
200
|
+
if (!req._isWebTransport) return;
|
|
201
|
+
if (typeof data === 'string') data = Buffer.from(data);
|
|
202
|
+
if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
203
|
+
var quarterSid = Math.floor(Number(streamId) / 4);
|
|
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);
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
createBidirectionalStream: function () {
|
|
212
|
+
if (!req._isWebTransport || !conn.h3) return null;
|
|
213
|
+
var wtStreamId = conn.h3.createWebTransportStream(Number(streamId), true);
|
|
214
|
+
return _createWtStream(conn, conn.quic, wtStreamId, Number(streamId), true);
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
createUnidirectionalStream: function () {
|
|
218
|
+
if (!req._isWebTransport || !conn.h3) return null;
|
|
219
|
+
var wtStreamId = conn.h3.createWebTransportStream(Number(streamId), false);
|
|
220
|
+
var stream = new Writable({
|
|
221
|
+
write: function (chunk, encoding, cb) {
|
|
222
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
|
|
223
|
+
conn.quic.sendStream(wtStreamId, new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength), false);
|
|
224
|
+
cb();
|
|
225
|
+
},
|
|
226
|
+
final: function (cb) {
|
|
227
|
+
conn.quic.sendStream(wtStreamId, new Uint8Array(0), true);
|
|
228
|
+
cb();
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
stream._streamId = wtStreamId;
|
|
232
|
+
return stream;
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
conn.http_streams[streamId] = { req: req, res: res };
|
|
237
|
+
is_new = true;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Apply params
|
|
241
|
+
if (typeof params === 'object') {
|
|
242
|
+
|
|
243
|
+
if ('request_headers' in params) {
|
|
244
|
+
var stream = conn.http_streams[streamId];
|
|
245
|
+
for (var name in params.request_headers) {
|
|
246
|
+
stream.req.headers[name] = params.request_headers[name];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Extract :method and :path
|
|
250
|
+
if (stream.req.method === null && ':method' in stream.req.headers) {
|
|
251
|
+
stream.req.method = stream.req.headers[':method'];
|
|
252
|
+
}
|
|
253
|
+
if (stream.req.url === null && ':path' in stream.req.headers) {
|
|
254
|
+
stream.req.url = stream.req.headers[':path'];
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if ('request_body_chunk' in params) {
|
|
259
|
+
var stream = conn.http_streams[streamId];
|
|
260
|
+
var listeners = stream.req._listeners['data'] || [];
|
|
261
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
262
|
+
try { listeners[i](params.request_body_chunk); } catch (e) { }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if ('request_end' in params) {
|
|
267
|
+
var stream = conn.http_streams[streamId];
|
|
268
|
+
var listeners = stream.req._listeners['end'] || [];
|
|
269
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
270
|
+
try { listeners[i](); } catch (e) { }
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Fire handler on new stream (after headers are set)
|
|
276
|
+
if (is_new) {
|
|
277
|
+
var stream = conn.http_streams[streamId];
|
|
278
|
+
if (typeof handler === 'function') {
|
|
279
|
+
handler(stream.req, stream.res);
|
|
280
|
+
}
|
|
281
|
+
ev.emit('request', stream.req, stream.res);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
// ============================================================
|
|
287
|
+
// UDP → connection routing
|
|
288
|
+
// ============================================================
|
|
289
|
+
|
|
290
|
+
function onUdpMessage(msg, rinfo) {
|
|
291
|
+
var data = new Uint8Array(msg);
|
|
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
|
+
});
|
|
347
|
+
|
|
348
|
+
// QUIC handshake complete → set up H3
|
|
349
|
+
quic.on('connect', function () {
|
|
350
|
+
var h3 = new H3Connection({ quicConnection: quic, enableWebTransport: true });
|
|
351
|
+
connections[connId].h3 = h3;
|
|
352
|
+
|
|
353
|
+
// HTTP request headers received → set_http_stream (reactive)
|
|
354
|
+
h3.on('http_headers', function (streamId, headers) {
|
|
355
|
+
set_http_stream(connId, streamId, { request_headers: headers });
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
// HTTP request body chunk → set_http_stream
|
|
359
|
+
h3.on('http_body', function (streamId, data) {
|
|
360
|
+
set_http_stream(connId, streamId, { request_body_chunk: data });
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// HTTP request complete → fire req.on('end')
|
|
364
|
+
h3.on('http_end', function (streamId) {
|
|
365
|
+
set_http_stream(connId, streamId, { request_end: true });
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
// ---- WebTransport stream events ----
|
|
369
|
+
|
|
370
|
+
// New WT stream (bidi or uni from client)
|
|
371
|
+
h3.on('wt_stream', function (sessionId, wtStreamId, data, fin, isBidi) {
|
|
372
|
+
var conn = connections[connId];
|
|
373
|
+
if (!conn) return;
|
|
374
|
+
var session = conn.http_streams[sessionId];
|
|
375
|
+
if (!session || !session.req._isWebTransport) return;
|
|
376
|
+
|
|
377
|
+
// Create a Duplex-like stream for the WT stream
|
|
378
|
+
var wtStream = _createWtStream(conn, quic, wtStreamId, sessionId, isBidi);
|
|
379
|
+
|
|
380
|
+
// Push initial data
|
|
381
|
+
if (data.byteLength > 0) _emitOnReq(session.req, 'data:' + wtStreamId, data);
|
|
382
|
+
if (fin) _emitOnReq(session.req, 'end:' + wtStreamId);
|
|
383
|
+
|
|
384
|
+
// Emit on req
|
|
385
|
+
if (isBidi) {
|
|
386
|
+
_emitOnReq(session.req, 'stream', wtStream);
|
|
387
|
+
} else {
|
|
388
|
+
_emitOnReq(session.req, 'unidirectionalStream', wtStream);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// If initial data, push to stream
|
|
392
|
+
if (data.byteLength > 0) wtStream._pushData(data);
|
|
393
|
+
if (fin) wtStream._endData();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
// Subsequent data on WT stream
|
|
397
|
+
h3.on('wt_data', function (sessionId, wtStreamId, data, fin) {
|
|
398
|
+
var conn = connections[connId];
|
|
399
|
+
if (!conn || !conn._wtStreamObjects) return;
|
|
400
|
+
var wtStream = conn._wtStreamObjects[wtStreamId];
|
|
401
|
+
if (!wtStream) return;
|
|
402
|
+
if (data.byteLength > 0) wtStream._pushData(data);
|
|
403
|
+
if (fin) { wtStream._endData(); delete conn._wtStreamObjects[wtStreamId]; }
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// Datagrams
|
|
407
|
+
quic.on('datagram', function (_contextId, rawData) {
|
|
408
|
+
if (!rawData || rawData.byteLength === 0) return;
|
|
409
|
+
var result = readVarInt(rawData, 0);
|
|
410
|
+
if (!result) return;
|
|
411
|
+
var quarterSid = result.value;
|
|
412
|
+
var payload = rawData.slice(result.byteLength);
|
|
413
|
+
|
|
414
|
+
// Find the session: quarter_stream_id = session_stream_id / 4
|
|
415
|
+
var conn = connections[connId];
|
|
416
|
+
if (!conn) return;
|
|
417
|
+
for (var sid in conn.http_streams) {
|
|
418
|
+
var session = conn.http_streams[sid];
|
|
419
|
+
if (session.req._isWebTransport && Math.floor(Number(sid) / 4) === quarterSid) {
|
|
420
|
+
_emitOnReq(session.req, 'datagram', Buffer.from(payload));
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
});
|
|
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
|
+
}
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
// ============================================================
|
|
511
|
+
// Server API
|
|
512
|
+
// ============================================================
|
|
513
|
+
|
|
514
|
+
return {
|
|
515
|
+
listen: listen,
|
|
516
|
+
close: close,
|
|
517
|
+
on: function (name, fn) { ev.on(name, fn); },
|
|
518
|
+
setTimeout: function () { /* TODO */ }
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
export { createServer };
|