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
|
@@ -0,0 +1,1238 @@
|
|
|
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 flat_ranges from 'flat-ranges';
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
Emitter,
|
|
19
|
+
concatUint8Arrays,
|
|
20
|
+
uint8Equal,
|
|
21
|
+
ack_frame_to_ranges,
|
|
22
|
+
ranges_to_ack_frame
|
|
23
|
+
} from './utils.js';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
quic_derive_init_secrets,
|
|
27
|
+
quic_derive_from_tls_secrets,
|
|
28
|
+
quic_derive_key_update,
|
|
29
|
+
encrypt_quic_packet,
|
|
30
|
+
decrypt_quic_packet,
|
|
31
|
+
extract_tls_messages_from_chunks
|
|
32
|
+
} from './crypto.js';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
parse_quic_datagram,
|
|
36
|
+
encode_quic_frames,
|
|
37
|
+
parse_quic_frames
|
|
38
|
+
} from './transport.js';
|
|
39
|
+
|
|
40
|
+
import { TLSBridge } from './tls_bridge.js';
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function QUICConnection(options) {
|
|
44
|
+
if (!(this instanceof QUICConnection)) return new QUICConnection(options);
|
|
45
|
+
options = options || {};
|
|
46
|
+
|
|
47
|
+
var ev = Emitter();
|
|
48
|
+
var tls = null;
|
|
49
|
+
|
|
50
|
+
// ============================================================
|
|
51
|
+
// Context
|
|
52
|
+
// ============================================================
|
|
53
|
+
|
|
54
|
+
var context = {
|
|
55
|
+
isServer: options.isServer !== false,
|
|
56
|
+
state: 'idle',
|
|
57
|
+
handshake_done: false,
|
|
58
|
+
handshake_done_sent: false,
|
|
59
|
+
|
|
60
|
+
version: 1,
|
|
61
|
+
original_dcid: null,
|
|
62
|
+
my_cids: [],
|
|
63
|
+
their_cids: [],
|
|
64
|
+
|
|
65
|
+
// Keys
|
|
66
|
+
initial_read: null,
|
|
67
|
+
initial_write: null,
|
|
68
|
+
handshake_read: null,
|
|
69
|
+
handshake_write: null,
|
|
70
|
+
app_read: null,
|
|
71
|
+
app_write: null,
|
|
72
|
+
app_prev_read: null, // previous read keys (kept during transition)
|
|
73
|
+
key_phase: false, // our current send key_phase
|
|
74
|
+
read_key_phase: false, // expected key_phase on incoming packets
|
|
75
|
+
app_read_secret: null, // current read secret (for deriving next)
|
|
76
|
+
app_write_secret: null, // current write secret (for deriving next)
|
|
77
|
+
cipher_hash: 'sha256', // hash for key derivation
|
|
78
|
+
cipher_suite: null, // TLS cipher suite code (for key length)
|
|
79
|
+
|
|
80
|
+
// Packet numbers — sending
|
|
81
|
+
send_pn: { initial: 0, handshake: 0, app: 0 },
|
|
82
|
+
|
|
83
|
+
// Packet numbers — receiving
|
|
84
|
+
recv_pn_largest: { initial: -1, handshake: -1, app: -1 },
|
|
85
|
+
recv_pn_ranges: { initial: [], handshake: [], app: [] },
|
|
86
|
+
|
|
87
|
+
// CRYPTO
|
|
88
|
+
crypto_chunks: { initial: {}, handshake: {} },
|
|
89
|
+
crypto_offset: { initial: 0, handshake: 0 },
|
|
90
|
+
crypto_send_offset: { initial: 0, handshake: 0, app: 0 },
|
|
91
|
+
|
|
92
|
+
// ACK
|
|
93
|
+
pending_ack: { initial: [], handshake: [], app: [] },
|
|
94
|
+
|
|
95
|
+
// Receiving streams
|
|
96
|
+
recv_streams: {},
|
|
97
|
+
|
|
98
|
+
// Sending streams
|
|
99
|
+
send_streams: {},
|
|
100
|
+
|
|
101
|
+
// In-flight tracking (Phase 1)
|
|
102
|
+
sending_app_pn_in_flight: new Set(),
|
|
103
|
+
sending_app_pn_history: [],
|
|
104
|
+
|
|
105
|
+
// Burst scheduler config (Phase 2)
|
|
106
|
+
max_packets_per_burst: 20,
|
|
107
|
+
max_packets_per_sec: 5000,
|
|
108
|
+
max_bytes_per_sec: 10000000, // 10MB/s
|
|
109
|
+
max_packet_payload: 1100,
|
|
110
|
+
max_packets_in_flight: 100,
|
|
111
|
+
max_bytes_in_flight: 5000000, // 5MB
|
|
112
|
+
burst_timer: null,
|
|
113
|
+
|
|
114
|
+
// Pending app packets
|
|
115
|
+
pending_app_packets: [],
|
|
116
|
+
|
|
117
|
+
// Flow control — connection level
|
|
118
|
+
bytes_sent: 0, // total STREAM bytes sent
|
|
119
|
+
bytes_received: 0, // total STREAM bytes received
|
|
120
|
+
remote_max_data: 1048576, // peer's limit on what we can send (default 1MB until parsed)
|
|
121
|
+
local_max_data: 1048576, // our limit on what peer can send (matches transport params)
|
|
122
|
+
local_max_data_consumed: 0, // how much of local_max_data has been used
|
|
123
|
+
local_max_data_threshold: 0.5, // send MAX_DATA when consumed > threshold × local_max_data
|
|
124
|
+
remote_max_streams_bidi: 100,
|
|
125
|
+
remote_max_streams_uni: 3,
|
|
126
|
+
|
|
127
|
+
// RTT
|
|
128
|
+
rtt_history: [],
|
|
129
|
+
|
|
130
|
+
// Timers
|
|
131
|
+
idle_timeout: options.idleTimeout || 30000,
|
|
132
|
+
handshake_timeout: options.handshakeTimeout || 10000,
|
|
133
|
+
last_activity: Date.now(),
|
|
134
|
+
idle_timer: null,
|
|
135
|
+
handshake_timer: null,
|
|
136
|
+
|
|
137
|
+
SNICallback: options.SNICallback || null,
|
|
138
|
+
hostname: options.hostname || null,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
// ============================================================
|
|
143
|
+
// set_context
|
|
144
|
+
// ============================================================
|
|
145
|
+
|
|
146
|
+
function set_context(updates) {
|
|
147
|
+
if (!updates || typeof updates !== 'object') return;
|
|
148
|
+
var changed = {};
|
|
149
|
+
|
|
150
|
+
if ('state' in updates && updates.state !== context.state) {
|
|
151
|
+
context.state = updates.state;
|
|
152
|
+
changed.state = true;
|
|
153
|
+
}
|
|
154
|
+
if ('handshake_done' in updates && updates.handshake_done !== context.handshake_done) {
|
|
155
|
+
context.handshake_done = updates.handshake_done;
|
|
156
|
+
changed.handshake_done = true;
|
|
157
|
+
}
|
|
158
|
+
if ('handshake_done_sent' in updates) {
|
|
159
|
+
context.handshake_done_sent = updates.handshake_done_sent;
|
|
160
|
+
}
|
|
161
|
+
if ('original_dcid' in updates && updates.original_dcid !== null && context.original_dcid === null) {
|
|
162
|
+
context.original_dcid = updates.original_dcid;
|
|
163
|
+
changed.original_dcid = true;
|
|
164
|
+
}
|
|
165
|
+
if ('version' in updates && updates.version !== context.version) {
|
|
166
|
+
context.version = updates.version;
|
|
167
|
+
}
|
|
168
|
+
if ('add_their_cid' in updates) {
|
|
169
|
+
var cid = updates.add_their_cid;
|
|
170
|
+
var found = false;
|
|
171
|
+
for (var i = 0; i < context.their_cids.length; i++) {
|
|
172
|
+
if (uint8Equal(cid, context.their_cids[i])) { found = true; break; }
|
|
173
|
+
}
|
|
174
|
+
if (!found) context.their_cids.push(cid);
|
|
175
|
+
}
|
|
176
|
+
if ('initial_read' in updates) { context.initial_read = updates.initial_read; changed.initial_read = true; }
|
|
177
|
+
if ('initial_write' in updates) { context.initial_write = updates.initial_write; changed.initial_write = true; }
|
|
178
|
+
if ('handshake_read' in updates) { context.handshake_read = updates.handshake_read; }
|
|
179
|
+
if ('handshake_write' in updates){ context.handshake_write = updates.handshake_write; }
|
|
180
|
+
if ('app_read' in updates) { context.app_read = updates.app_read; }
|
|
181
|
+
if ('app_write' in updates) { context.app_write = updates.app_write; }
|
|
182
|
+
if ('remote_max_data' in updates && updates.remote_max_data > context.remote_max_data) {
|
|
183
|
+
context.remote_max_data = updates.remote_max_data; changed.remote_max_data = true;
|
|
184
|
+
}
|
|
185
|
+
if ('remote_max_streams_bidi' in updates) { context.remote_max_streams_bidi = updates.remote_max_streams_bidi; }
|
|
186
|
+
if ('remote_max_streams_uni' in updates) { context.remote_max_streams_uni = updates.remote_max_streams_uni; }
|
|
187
|
+
if ('key_phase' in updates && updates.key_phase !== context.key_phase) {
|
|
188
|
+
context.key_phase = updates.key_phase; changed.key_phase = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---- Reactive ----
|
|
192
|
+
|
|
193
|
+
if (changed.initial_read || changed.initial_write) {
|
|
194
|
+
if (context.state === 'idle') { context.state = 'handshaking'; changed.state = true; }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (changed.handshake_done && context.handshake_done === true) {
|
|
198
|
+
console.log('[quic] handshake done — flushing ' + context.pending_app_packets.length + ' pending');
|
|
199
|
+
if (context.pending_app_packets.length > 0) {
|
|
200
|
+
var pending = context.pending_app_packets;
|
|
201
|
+
context.pending_app_packets = [];
|
|
202
|
+
for (var i = 0; i < pending.length; i++) {
|
|
203
|
+
processDecryptedPacket('app', pending[i].packet_number, pending[i].plaintext);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
startIdleTimer();
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (changed.state && context.state === 'connected') {
|
|
210
|
+
// 'connect' event is emitted explicitly by TLS handlers (not here)
|
|
211
|
+
// Server emits on appSecrets, Client emits on secureConnect
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (changed.state && context.state === 'draining') {
|
|
215
|
+
clearIdleTimer();
|
|
216
|
+
var _drainTimer = setTimeout(function () {
|
|
217
|
+
if (context.state === 'draining') { context.state = 'closed'; ev.emit('close'); }
|
|
218
|
+
}, Math.min(3000, context.idle_timeout / 3));
|
|
219
|
+
if (_drainTimer.unref) _drainTimer.unref();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (changed.state && context.state === 'closed') { clearIdleTimer(); ev.emit('close'); }
|
|
223
|
+
if (changed.remote_max_data) { plan_quic_burst(); }
|
|
224
|
+
if (changed.key_phase) {
|
|
225
|
+
// We initiated key update — derive new write keys
|
|
226
|
+
if (context.app_write_secret) {
|
|
227
|
+
var next = quic_derive_key_update(context.app_write_secret, context.cipher_hash, context.cipher_suite);
|
|
228
|
+
console.log('[quic] key update initiated — new write keys');
|
|
229
|
+
context.app_write = { key: next.key, iv: next.iv, hp: context.app_write.hp };
|
|
230
|
+
context.app_write_secret = next.secret;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
// ============================================================
|
|
237
|
+
// Idle timeout
|
|
238
|
+
// ============================================================
|
|
239
|
+
|
|
240
|
+
function touchActivity() { context.last_activity = Date.now(); }
|
|
241
|
+
|
|
242
|
+
function startIdleTimer() {
|
|
243
|
+
clearIdleTimer();
|
|
244
|
+
if (context.idle_timeout <= 0) return;
|
|
245
|
+
context.idle_timer = setInterval(function () {
|
|
246
|
+
if (Date.now() - context.last_activity >= context.idle_timeout) {
|
|
247
|
+
console.log('[quic] idle timeout');
|
|
248
|
+
close(0, 'idle timeout');
|
|
249
|
+
}
|
|
250
|
+
}, Math.max(1000, Math.floor(context.idle_timeout / 4)));
|
|
251
|
+
if (context.idle_timer.unref) context.idle_timer.unref();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function clearIdleTimer() {
|
|
255
|
+
if (context.idle_timer !== null) { clearInterval(context.idle_timer); context.idle_timer = null; }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
// ============================================================
|
|
260
|
+
// TLS Bridge
|
|
261
|
+
// ============================================================
|
|
262
|
+
|
|
263
|
+
function initTLS() {
|
|
264
|
+
// Start handshake timeout — if TLS doesn't complete in time, close
|
|
265
|
+
if (context.handshake_timeout > 0 && !context.handshake_timer) {
|
|
266
|
+
context.handshake_timer = setTimeout(function () {
|
|
267
|
+
if (context.state !== 'connected' && context.state !== 'closed' && context.state !== 'draining') {
|
|
268
|
+
console.log('[quic] handshake timeout (' + context.handshake_timeout + 'ms)');
|
|
269
|
+
ev.emit('error', new Error('QUIC handshake timeout'));
|
|
270
|
+
close(0x100, 'handshake timeout'); // 0x100 = CRYPTO_ERROR
|
|
271
|
+
}
|
|
272
|
+
}, context.handshake_timeout);
|
|
273
|
+
if (context.handshake_timer.unref) context.handshake_timer.unref();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
tls = new TLSBridge({
|
|
277
|
+
isServer: context.isServer,
|
|
278
|
+
SNICallback: context.SNICallback,
|
|
279
|
+
originalDcid: context.original_dcid,
|
|
280
|
+
localCid: context.my_cids.length > 0 ? context.my_cids[0] : new Uint8Array(0),
|
|
281
|
+
hostname: context.hostname
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
tls.on('send', function (epoch, data) { cryptoWrite(epoch, data); });
|
|
285
|
+
|
|
286
|
+
tls.on('handshakeSecrets', function (secrets) {
|
|
287
|
+
set_context({
|
|
288
|
+
handshake_read: quic_derive_from_tls_secrets(secrets.remote, secrets.hash, secrets.cipher),
|
|
289
|
+
handshake_write: quic_derive_from_tls_secrets(secrets.local, secrets.hash, secrets.cipher)
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
tls.on('appSecrets', function (secrets) {
|
|
294
|
+
// Handshake complete — clear timeout
|
|
295
|
+
if (context.handshake_timer) {
|
|
296
|
+
clearTimeout(context.handshake_timer);
|
|
297
|
+
context.handshake_timer = null;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Store secrets for future key updates (RFC 9001 §6)
|
|
301
|
+
context.app_read_secret = secrets.remote;
|
|
302
|
+
context.app_write_secret = secrets.local;
|
|
303
|
+
context.cipher_hash = secrets.hash;
|
|
304
|
+
context.cipher_suite = secrets.cipher;
|
|
305
|
+
|
|
306
|
+
set_context({
|
|
307
|
+
app_read: quic_derive_from_tls_secrets(secrets.remote, secrets.hash, secrets.cipher),
|
|
308
|
+
app_write: quic_derive_from_tls_secrets(secrets.local, secrets.hash, secrets.cipher),
|
|
309
|
+
state: 'connected',
|
|
310
|
+
handshake_done: true
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// Server: emit connect now (Finished already sent)
|
|
314
|
+
// Client: wait for secureConnect (after client Finished is generated)
|
|
315
|
+
if (context.isServer) {
|
|
316
|
+
ev.emit('connect');
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
tls.on('secureConnect', function () {
|
|
321
|
+
// Client: Finished has been generated and queued.
|
|
322
|
+
// Flush handshake first, then emit connect so 1-RTT goes after Finished.
|
|
323
|
+
if (!context.isServer) {
|
|
324
|
+
plan_quic_burst(); // flush any pending handshake data (Finished)
|
|
325
|
+
ev.emit('connect');
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
// Note: QUIC handles key updates via key_phase bit, not TLS KeyUpdate message.
|
|
330
|
+
// RFC 9001 §6: "endpoints MUST treat the receipt of a TLS KeyUpdate message
|
|
331
|
+
// as a connection error of type 0x010a" — but we silently ignore it.
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
// ============================================================
|
|
336
|
+
// Incoming packets
|
|
337
|
+
// ============================================================
|
|
338
|
+
|
|
339
|
+
function feedDatagram(from_ip, from_port, data) {
|
|
340
|
+
console.log('[quic] datagram from ' + from_ip + ':' + from_port + ' len=' + data.length);
|
|
341
|
+
touchActivity();
|
|
342
|
+
var packets = parse_quic_datagram(data);
|
|
343
|
+
for (var i = 0; i < packets.length; i++) {
|
|
344
|
+
if (packets[i] !== null) feedPacket(packets[i]);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function feedPacket(pkt) {
|
|
349
|
+
if (context.state === 'draining' || context.state === 'closed') return;
|
|
350
|
+
|
|
351
|
+
if (pkt.version && pkt.version !== context.version) set_context({ version: pkt.version });
|
|
352
|
+
if (pkt.dcid && pkt.dcid.byteLength > 0) set_context({ original_dcid: pkt.dcid });
|
|
353
|
+
if (pkt.scid && pkt.scid.byteLength > 0) set_context({ add_their_cid: pkt.scid });
|
|
354
|
+
|
|
355
|
+
var space = pkt.type === 'initial' ? 'initial'
|
|
356
|
+
: pkt.type === 'handshake' ? 'handshake'
|
|
357
|
+
: pkt.type === '1rtt' ? 'app' : null;
|
|
358
|
+
if (!space) return;
|
|
359
|
+
|
|
360
|
+
if (space === 'initial' && !context.initial_read && context.original_dcid) {
|
|
361
|
+
set_context({
|
|
362
|
+
initial_read: quic_derive_init_secrets(context.original_dcid, context.version, 'read'),
|
|
363
|
+
initial_write: quic_derive_init_secrets(context.original_dcid, context.version, 'write')
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
var readKeys = space === 'initial' ? context.initial_read
|
|
368
|
+
: space === 'handshake' ? context.handshake_read : context.app_read;
|
|
369
|
+
if (!readKeys) return;
|
|
370
|
+
|
|
371
|
+
var decrypted = decrypt_quic_packet(
|
|
372
|
+
pkt.raw, readKeys.key, readKeys.iv, readKeys.hp,
|
|
373
|
+
context.original_dcid, context.recv_pn_largest[space]
|
|
374
|
+
);
|
|
375
|
+
|
|
376
|
+
// Key Update: if app decrypt fails, try with derived next keys
|
|
377
|
+
if ((!decrypted || !decrypted.plaintext) && space === 'app' && context.app_read_secret) {
|
|
378
|
+
var next = quic_derive_key_update(context.app_read_secret, context.cipher_hash, context.cipher_suite);
|
|
379
|
+
decrypted = decrypt_quic_packet(
|
|
380
|
+
pkt.raw, next.key, next.iv, readKeys.hp, // HP doesn't change
|
|
381
|
+
context.original_dcid, context.recv_pn_largest[space]
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
if (decrypted && decrypted.plaintext && decrypted.plaintext.byteLength > 0) {
|
|
385
|
+
// Key update confirmed — install new read keys
|
|
386
|
+
console.log('[quic] key update detected — installing new read keys');
|
|
387
|
+
context.app_prev_read = context.app_read;
|
|
388
|
+
context.app_read = { key: next.key, iv: next.iv, hp: readKeys.hp };
|
|
389
|
+
context.app_read_secret = next.secret;
|
|
390
|
+
context.read_key_phase = !context.read_key_phase;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Also try prev keys for reordered packets from before key update
|
|
395
|
+
if ((!decrypted || !decrypted.plaintext) && space === 'app' && context.app_prev_read) {
|
|
396
|
+
decrypted = decrypt_quic_packet(
|
|
397
|
+
pkt.raw, context.app_prev_read.key, context.app_prev_read.iv, context.app_prev_read.hp,
|
|
398
|
+
context.original_dcid, context.recv_pn_largest[space]
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!decrypted || !decrypted.plaintext || decrypted.plaintext.byteLength === 0) {
|
|
403
|
+
console.log('[quic] decrypt failed: ' + space + ' raw_len=' + pkt.raw.byteLength + ' first20=' + Array.from(pkt.raw.slice(0, 20)).map(function(b){ return b.toString(16).padStart(2,'0'); }).join(' ') + ' dcid_len=' + (context.original_dcid ? context.original_dcid.byteLength : 'null') + ' has_keys=' + !!readKeys + ' largest_pn=' + context.recv_pn_largest[space]);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
console.log('[quic] decrypted ' + space + ' pn=' + decrypted.packet_number + ' len=' + decrypted.plaintext.byteLength);
|
|
408
|
+
|
|
409
|
+
var pn = decrypted.packet_number;
|
|
410
|
+
var ranges = context.recv_pn_ranges[space];
|
|
411
|
+
var isNew = true;
|
|
412
|
+
for (var ri = 0; ri < ranges.length; ri += 2) {
|
|
413
|
+
if (pn >= ranges[ri] && pn < ranges[ri + 1]) { isNew = false; break; }
|
|
414
|
+
}
|
|
415
|
+
if (isNew) {
|
|
416
|
+
flat_ranges.add(ranges, [pn, pn + 1]);
|
|
417
|
+
if (pn > context.recv_pn_largest[space]) context.recv_pn_largest[space] = pn;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
console.log('[quic] pn=' + pn + ' isNew=' + isNew);
|
|
421
|
+
if (!isNew) return;
|
|
422
|
+
|
|
423
|
+
if (space === 'app' && !context.handshake_done) {
|
|
424
|
+
context.pending_app_packets.push(decrypted);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
processDecryptedPacket(space, pn, decrypted.plaintext);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
// ============================================================
|
|
433
|
+
// Frame processing
|
|
434
|
+
// ============================================================
|
|
435
|
+
|
|
436
|
+
function processDecryptedPacket(space, packetNumber, plaintext) {
|
|
437
|
+
var frames = parse_quic_frames(plaintext);
|
|
438
|
+
console.log('[quic] frames: ' + space + ' pn=' + packetNumber + ' [' + frames.map(function(f){ return f.type; }).join(',') + ']');
|
|
439
|
+
var ackEliciting = false;
|
|
440
|
+
|
|
441
|
+
if (context.isServer && space === 'app' && !context.handshake_done_sent) {
|
|
442
|
+
set_context({ handshake_done_sent: true });
|
|
443
|
+
sendFrames('app', [{ type: 'handshake_done' }]);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
for (var i = 0; i < frames.length; i++) {
|
|
447
|
+
var frame = frames[i];
|
|
448
|
+
|
|
449
|
+
if (frame.type === 'crypto') {
|
|
450
|
+
ackEliciting = true;
|
|
451
|
+
processCryptoFrame(space, frame.offset, frame.data);
|
|
452
|
+
} else if (frame.type === 'stream') {
|
|
453
|
+
ackEliciting = true;
|
|
454
|
+
processStreamFrame(frame);
|
|
455
|
+
} else if (frame.type === 'ack') {
|
|
456
|
+
processAckFrame(space, frame);
|
|
457
|
+
} else if (frame.type === 'ping') {
|
|
458
|
+
ackEliciting = true;
|
|
459
|
+
} else if (frame.type === 'handshake_done') {
|
|
460
|
+
ackEliciting = true;
|
|
461
|
+
} else if (frame.type === 'path_challenge') {
|
|
462
|
+
ackEliciting = true;
|
|
463
|
+
sendFrames(space, [{ type: 'path_response', data: frame.data }]);
|
|
464
|
+
} else if (frame.type === 'new_connection_id') {
|
|
465
|
+
ackEliciting = true;
|
|
466
|
+
} else if (frame.type === 'connection_close') {
|
|
467
|
+
console.log('[quic] CONNECTION_CLOSE error=0x' + (frame.error || 0).toString(16) + ' frame_type=0x' + (frame.frameType || 0).toString(16) + ' reason="' + (frame.reason || '') + '"');
|
|
468
|
+
set_context({ state: 'draining' });
|
|
469
|
+
return;
|
|
470
|
+
} else if (frame.type === 'stop_sending') {
|
|
471
|
+
// Peer says: stop sending on this stream
|
|
472
|
+
ackEliciting = true;
|
|
473
|
+
if (frame.id in context.send_streams) {
|
|
474
|
+
console.log('[quic] STOP_SENDING stream=' + frame.id + ' error=' + frame.error);
|
|
475
|
+
delete context.send_streams[frame.id];
|
|
476
|
+
}
|
|
477
|
+
} else if (frame.type === 'reset_stream') {
|
|
478
|
+
// Peer cancelled their stream
|
|
479
|
+
ackEliciting = true;
|
|
480
|
+
if (frame.id in context.recv_streams) {
|
|
481
|
+
console.log('[quic] RESET_STREAM stream=' + frame.id + ' error=' + frame.error);
|
|
482
|
+
delete context.recv_streams[frame.id];
|
|
483
|
+
}
|
|
484
|
+
} else if (frame.type === 'max_data') {
|
|
485
|
+
ackEliciting = true;
|
|
486
|
+
set_context({ remote_max_data: frame.max });
|
|
487
|
+
} else if (frame.type === 'max_streams_bidi') {
|
|
488
|
+
ackEliciting = true;
|
|
489
|
+
set_context({ remote_max_streams_bidi: frame.max });
|
|
490
|
+
} else if (frame.type === 'max_streams_uni') {
|
|
491
|
+
ackEliciting = true;
|
|
492
|
+
set_context({ remote_max_streams_uni: frame.max });
|
|
493
|
+
} else if (frame.type === 'max_stream_data') {
|
|
494
|
+
ackEliciting = true;
|
|
495
|
+
} else if (frame.type === 'datagram') {
|
|
496
|
+
ev.emit('datagram', frame.contextId, frame.data);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (ackEliciting) {
|
|
501
|
+
flat_ranges.add(context.pending_ack[space], [packetNumber, packetNumber + 1]);
|
|
502
|
+
var ackFrame = ranges_to_ack_frame(context.pending_ack[space], null, 0);
|
|
503
|
+
if (ackFrame) {
|
|
504
|
+
sendFrames(space, [ackFrame]);
|
|
505
|
+
if (space !== 'app') context.pending_ack[space] = [];
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
// ============================================================
|
|
512
|
+
// CRYPTO → TLS
|
|
513
|
+
// ============================================================
|
|
514
|
+
|
|
515
|
+
function processCryptoFrame(space, offset, data) {
|
|
516
|
+
if (space !== 'initial' && space !== 'handshake') return;
|
|
517
|
+
console.log('[quic] CRYPTO: space=' + space + ' offset=' + offset + ' len=' + data.length);
|
|
518
|
+
|
|
519
|
+
var chunks = context.crypto_chunks[space];
|
|
520
|
+
var fromOffset = context.crypto_offset[space];
|
|
521
|
+
if (!(offset in chunks) || chunks[offset].byteLength < data.byteLength) chunks[offset] = data;
|
|
522
|
+
|
|
523
|
+
var result = extract_tls_messages_from_chunks(chunks, fromOffset);
|
|
524
|
+
console.log('[quic] TLS messages: ' + (result ? result.tls_messages.length : 'none'));
|
|
525
|
+
if (!result) return;
|
|
526
|
+
|
|
527
|
+
context.crypto_offset[space] = result.new_from_offset;
|
|
528
|
+
if (!tls) { console.log('[quic] initializing TLS bridge'); initTLS(); }
|
|
529
|
+
|
|
530
|
+
for (var i = 0; i < result.tls_messages.length; i++) {
|
|
531
|
+
var msg = result.tls_messages[i];
|
|
532
|
+
var msgType = msg[0];
|
|
533
|
+
console.log('[quic] → TLS msg #' + i + ' type=0x' + msgType.toString(16) + ' len=' + msg.length);
|
|
534
|
+
|
|
535
|
+
// Detect HelloRetryRequest (type 0x02 with special random)
|
|
536
|
+
if (msgType === 0x02 && msg.length >= 38) {
|
|
537
|
+
var hrrRandom = [0xCF,0x21,0xAD,0x74,0xE5,0x9A,0x61,0x11,0xBE,0x1D,0x8C,0x02,0x1E,0x65,0xB8,0x91,0xC2,0xA2,0x11,0x16,0x7A,0xBB,0x8C,0x5E,0x07,0x9E,0x09,0xE2,0xC8,0xA8,0x33,0x9C];
|
|
538
|
+
var isHRR = true;
|
|
539
|
+
for (var j = 0; j < 32; j++) { if (msg[6 + j] !== hrrRandom[j]) { isHRR = false; break; } }
|
|
540
|
+
if (isHRR) console.log('[quic] ⚠️ HelloRetryRequest detected! LemonTLS needs HRR support.');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
tls.feedMessage(msg);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
// ============================================================
|
|
549
|
+
// Stream receive
|
|
550
|
+
// ============================================================
|
|
551
|
+
|
|
552
|
+
function processStreamFrame(frame) {
|
|
553
|
+
var sid = frame.id;
|
|
554
|
+
if (!(sid in context.recv_streams)) {
|
|
555
|
+
context.recv_streams[sid] = { chunks: {}, ranges: [], total_size: 0, flushed_to: 0 };
|
|
556
|
+
}
|
|
557
|
+
var stream = context.recv_streams[sid];
|
|
558
|
+
|
|
559
|
+
var alreadyHave = false;
|
|
560
|
+
for (var ri = 0; ri < stream.ranges.length; ri += 2) {
|
|
561
|
+
if (frame.offset >= stream.ranges[ri] && frame.offset + frame.data.length <= stream.ranges[ri + 1]) {
|
|
562
|
+
alreadyHave = true; break;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
if (!alreadyHave) {
|
|
566
|
+
flat_ranges.add(stream.ranges, [frame.offset, frame.offset + frame.data.length]);
|
|
567
|
+
if (!(frame.offset in stream.chunks) || stream.chunks[frame.offset].byteLength < frame.data.byteLength) {
|
|
568
|
+
stream.chunks[frame.offset] = frame.data;
|
|
569
|
+
}
|
|
570
|
+
// Track connection-level bytes received
|
|
571
|
+
context.bytes_received += frame.data.byteLength;
|
|
572
|
+
checkLocalFlowControl();
|
|
573
|
+
}
|
|
574
|
+
if (frame.fin && stream.total_size === 0) stream.total_size = frame.offset + frame.data.length;
|
|
575
|
+
flushStream(sid);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Check if we need to send MAX_DATA to give the peer more send capacity.
|
|
580
|
+
* When consumed > threshold × window, double the window and notify peer.
|
|
581
|
+
*/
|
|
582
|
+
function checkLocalFlowControl() {
|
|
583
|
+
if (context.bytes_received > context.local_max_data * context.local_max_data_threshold) {
|
|
584
|
+
var newMax = context.local_max_data * 2;
|
|
585
|
+
context.local_max_data = newMax;
|
|
586
|
+
sendFrames('app', [{ type: 'max_data', max: newMax }]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function flushStream(sid) {
|
|
591
|
+
var stream = context.recv_streams[sid];
|
|
592
|
+
if (!stream) return;
|
|
593
|
+
var parts = [];
|
|
594
|
+
var offset = stream.flushed_to;
|
|
595
|
+
while (offset in stream.chunks) {
|
|
596
|
+
var chunk = stream.chunks[offset];
|
|
597
|
+
delete stream.chunks[offset];
|
|
598
|
+
parts.push(chunk);
|
|
599
|
+
offset += chunk.byteLength;
|
|
600
|
+
}
|
|
601
|
+
if (parts.length === 0) return;
|
|
602
|
+
stream.flushed_to = offset;
|
|
603
|
+
var data = parts.length === 1 ? parts[0] : concatUint8Arrays(parts);
|
|
604
|
+
var fin = (stream.total_size > 0 && offset >= stream.total_size);
|
|
605
|
+
ev.emit('stream', Number(sid), data, fin);
|
|
606
|
+
if (fin) setTimeout(function () { delete context.recv_streams[sid]; }, 100);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
// ============================================================
|
|
611
|
+
// ACK processing (Phase 1)
|
|
612
|
+
// ============================================================
|
|
613
|
+
|
|
614
|
+
function processAckFrame(space, frame) {
|
|
615
|
+
var ackedRanges = ack_frame_to_ranges(frame);
|
|
616
|
+
if (!ackedRanges || ackedRanges.length === 0) return;
|
|
617
|
+
|
|
618
|
+
if (space === 'app') {
|
|
619
|
+
// RTT measurement
|
|
620
|
+
if ('largest' in frame && 'delay' in frame) {
|
|
621
|
+
var largest_pn = frame.largest;
|
|
622
|
+
if (context.sending_app_pn_in_flight.has(largest_pn)) {
|
|
623
|
+
var now = Date.now();
|
|
624
|
+
var ack_delay_ms = Math.round((frame.delay * Math.pow(2, 3)) / 1000);
|
|
625
|
+
var pn_index = largest_pn - (context.send_pn.app - context.sending_app_pn_history.length);
|
|
626
|
+
if (pn_index >= 0 && pn_index < context.sending_app_pn_history.length) {
|
|
627
|
+
var measured_rtt = now - context.sending_app_pn_history[pn_index][0] - ack_delay_ms;
|
|
628
|
+
if (measured_rtt > 0) context.rtt_history.push([now, measured_rtt]);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Mark in-flight as acked
|
|
634
|
+
for (var pn of context.sending_app_pn_in_flight) {
|
|
635
|
+
var is_acked = false;
|
|
636
|
+
for (var ri = 0; ri < ackedRanges.length; ri += 2) {
|
|
637
|
+
if (pn >= ackedRanges[ri] && pn <= ackedRanges[ri + 1]) { is_acked = true; break; }
|
|
638
|
+
}
|
|
639
|
+
if (is_acked) {
|
|
640
|
+
context.sending_app_pn_in_flight.delete(pn);
|
|
641
|
+
for (var sid in context.send_streams) {
|
|
642
|
+
var st = context.send_streams[sid];
|
|
643
|
+
if (st.in_flight_ranges && pn in st.in_flight_ranges) {
|
|
644
|
+
flat_ranges.add(st.acked_ranges, st.in_flight_ranges[pn]);
|
|
645
|
+
delete st.in_flight_ranges[pn];
|
|
646
|
+
if (st.total_size > 0 && st.acked_ranges.length === 2 &&
|
|
647
|
+
st.acked_ranges[0] === 0 && st.acked_ranges[1] >= st.total_size) {
|
|
648
|
+
delete context.send_streams[sid];
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ACK freed capacity — try to send more
|
|
656
|
+
plan_quic_burst();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
// ============================================================
|
|
662
|
+
// Sending
|
|
663
|
+
// ============================================================
|
|
664
|
+
|
|
665
|
+
function cryptoWrite(epoch, data) {
|
|
666
|
+
var space = epoch === 'initial' ? 'initial' : epoch === 'handshake' ? 'handshake' : 'app';
|
|
667
|
+
var offset = context.crypto_send_offset[space];
|
|
668
|
+
context.crypto_send_offset[space] += data.byteLength;
|
|
669
|
+
sendFrames(space, [{ type: 'crypto', offset: offset, data: data }]);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function sendFrames(space, frameList) {
|
|
673
|
+
var writeKeys = space === 'initial' ? context.initial_write
|
|
674
|
+
: space === 'handshake' ? context.handshake_write : context.app_write;
|
|
675
|
+
if (!writeKeys) { console.log('[quic] sendFrames(' + space + ') — no keys'); return; }
|
|
676
|
+
|
|
677
|
+
var pn = context.send_pn[space]++;
|
|
678
|
+
var packetType = space === 'initial' ? 'initial' : space === 'handshake' ? 'handshake' : '1rtt';
|
|
679
|
+
|
|
680
|
+
var dcid, scid;
|
|
681
|
+
if (packetType === '1rtt') {
|
|
682
|
+
dcid = context.their_cids.length > 0 ? context.their_cids[0] : new Uint8Array(0);
|
|
683
|
+
scid = new Uint8Array(0);
|
|
684
|
+
} else if (context.isServer) {
|
|
685
|
+
dcid = context.their_cids.length > 0 ? context.their_cids[0] : new Uint8Array(0);
|
|
686
|
+
scid = context.original_dcid || new Uint8Array(0);
|
|
687
|
+
} else {
|
|
688
|
+
// Client: Initial uses original_dcid (for key derivation),
|
|
689
|
+
// Handshake uses server's SCID (RFC 9000 §7.2)
|
|
690
|
+
if (space === 'initial' || context.their_cids.length === 0) {
|
|
691
|
+
dcid = context.original_dcid || new Uint8Array(0);
|
|
692
|
+
} else {
|
|
693
|
+
dcid = context.their_cids[0];
|
|
694
|
+
}
|
|
695
|
+
scid = context.my_cids.length > 0 ? context.my_cids[0] : new Uint8Array(0);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
var encoded = encode_quic_frames(frameList);
|
|
699
|
+
|
|
700
|
+
// RFC 9000 §14.1: Initial datagrams must be >= 1200 bytes
|
|
701
|
+
if (space === 'initial') {
|
|
702
|
+
var overhead = 1 + 4 + 1 + dcid.byteLength + 1 + scid.byteLength + 1 + 2 + 1 + 16;
|
|
703
|
+
var minPayload = 1200 - overhead;
|
|
704
|
+
if (encoded.length < minPayload) {
|
|
705
|
+
encoded = concatUint8Arrays([encoded, new Uint8Array(minPayload - encoded.length)]);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
var encrypted = encrypt_quic_packet(
|
|
710
|
+
packetType, encoded, writeKeys.key, writeKeys.iv, writeKeys.hp, pn, dcid, scid, null,
|
|
711
|
+
space === 'app' ? context.key_phase : false
|
|
712
|
+
);
|
|
713
|
+
|
|
714
|
+
if (encrypted) {
|
|
715
|
+
var fnames = frameList.map(function(f){ return f.type + (f.type === 'ack' ? '(lg=' + f.largest + ')' : ''); }).join(',');
|
|
716
|
+
console.log('[quic] → ' + packetType + ' pn=' + pn + ' frames=[' + fnames + '] len=' + encrypted.length);
|
|
717
|
+
touchActivity();
|
|
718
|
+
|
|
719
|
+
if (space === 'app') {
|
|
720
|
+
context.sending_app_pn_history.push([Date.now(), encoded.length]);
|
|
721
|
+
var has_data = false;
|
|
722
|
+
for (var i = 0; i < frameList.length; i++) {
|
|
723
|
+
if (frameList[i].type === 'stream' || frameList[i].type === 'crypto') { has_data = true; break; }
|
|
724
|
+
}
|
|
725
|
+
if (has_data) context.sending_app_pn_in_flight.add(pn);
|
|
726
|
+
|
|
727
|
+
for (var i = 0; i < frameList.length; i++) {
|
|
728
|
+
if (frameList[i].type === 'stream') {
|
|
729
|
+
var sid = frameList[i].id;
|
|
730
|
+
var dataLen = frameList[i].data ? frameList[i].data.byteLength : 0;
|
|
731
|
+
|
|
732
|
+
// Track connection-level bytes sent
|
|
733
|
+
context.bytes_sent += dataLen;
|
|
734
|
+
|
|
735
|
+
if (sid in context.send_streams) {
|
|
736
|
+
if (!context.send_streams[sid].in_flight_ranges) context.send_streams[sid].in_flight_ranges = {};
|
|
737
|
+
var from = frameList[i].offset;
|
|
738
|
+
var to = from + (frameList[i].data ? frameList[i].data.byteLength : 0);
|
|
739
|
+
if (!context.send_streams[sid].in_flight_ranges[pn]) {
|
|
740
|
+
context.send_streams[sid].in_flight_ranges[pn] = [from, to];
|
|
741
|
+
} else {
|
|
742
|
+
flat_ranges.add(context.send_streams[sid].in_flight_ranges[pn], [from, to]);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
ev.emit('packet', encrypted);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
|
|
754
|
+
// ============================================================
|
|
755
|
+
// Stream send + Burst scheduler (Phase 2)
|
|
756
|
+
// Adapted from old quic_socket.js, improved with set_context
|
|
757
|
+
// ============================================================
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Buffer data for a stream. Does NOT send immediately.
|
|
761
|
+
* Calls plan_quic_burst() to schedule sending.
|
|
762
|
+
*/
|
|
763
|
+
function set_sending_stream(streamId, options) {
|
|
764
|
+
if (!(streamId in context.send_streams)) {
|
|
765
|
+
context.send_streams[streamId] = {
|
|
766
|
+
pending_data: null, pending_offset_start: 0,
|
|
767
|
+
write_offset: 0, send_offset: 0, total_size: 0,
|
|
768
|
+
fin_sent: false, acked_ranges: [], in_flight_ranges: {}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
var stream = context.send_streams[streamId];
|
|
773
|
+
|
|
774
|
+
if (typeof options === 'object' && 'add_chunk' in options) {
|
|
775
|
+
if (options.add_chunk.data === null || options.add_chunk.data === undefined) {
|
|
776
|
+
if (stream.total_size === 0) {
|
|
777
|
+
stream.total_size = stream.write_offset;
|
|
778
|
+
console.log('[quic] stream ' + streamId + ' FIN via null data, total_size=' + stream.total_size);
|
|
779
|
+
}
|
|
780
|
+
} else {
|
|
781
|
+
var chunk = options.add_chunk.data;
|
|
782
|
+
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
783
|
+
var start = stream.write_offset;
|
|
784
|
+
stream.write_offset += chunk.byteLength;
|
|
785
|
+
console.log('[quic] stream ' + streamId + ' add_chunk len=' + chunk.byteLength + ' write_offset=' + stream.write_offset + ' fin=' + !!options.add_chunk.fin);
|
|
786
|
+
|
|
787
|
+
if (stream.pending_data === null) {
|
|
788
|
+
stream.pending_data = chunk;
|
|
789
|
+
stream.pending_offset_start = start;
|
|
790
|
+
} else {
|
|
791
|
+
var old = stream.pending_data, old_off = stream.pending_offset_start;
|
|
792
|
+
var ns = Math.min(old_off, start);
|
|
793
|
+
var ne = Math.max(old_off + old.length, start + chunk.length);
|
|
794
|
+
var merged = new Uint8Array(ne - ns);
|
|
795
|
+
merged.set(old, old_off - ns);
|
|
796
|
+
merged.set(chunk, start - ns);
|
|
797
|
+
stream.pending_data = merged;
|
|
798
|
+
stream.pending_offset_start = ns;
|
|
799
|
+
}
|
|
800
|
+
if (options.add_chunk.fin) {
|
|
801
|
+
stream.total_size = stream.write_offset;
|
|
802
|
+
console.log('[quic] stream ' + streamId + ' FIN via add_chunk.fin, total_size=' + stream.total_size);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// Don't send directly — let burst scheduler handle it
|
|
808
|
+
plan_quic_burst();
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function sendStream(streamId, data, fin) {
|
|
812
|
+
set_sending_stream(streamId, { add_chunk: { data: data, fin: fin } });
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
function sendDatagram(data) {
|
|
816
|
+
if (context.state !== 'connected') return;
|
|
817
|
+
if (typeof data === 'string') data = new TextEncoder().encode(data);
|
|
818
|
+
sendFrames('app', [{ type: 'datagram', data: data }]);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
/**
|
|
823
|
+
* plan_quic_burst — calculates how many packets we can send,
|
|
824
|
+
* calls execute_quic_burst, schedules next burst if needed.
|
|
825
|
+
*/
|
|
826
|
+
function plan_quic_burst() {
|
|
827
|
+
if (!context.app_write) return;
|
|
828
|
+
if (context.state !== 'connected') return;
|
|
829
|
+
|
|
830
|
+
// Cancel pending timer
|
|
831
|
+
if (context.burst_timer !== null) {
|
|
832
|
+
clearTimeout(context.burst_timer);
|
|
833
|
+
clearImmediate(context.burst_timer);
|
|
834
|
+
context.burst_timer = null;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
var now = Date.now();
|
|
838
|
+
var oneSecAgo = now - 1000;
|
|
839
|
+
|
|
840
|
+
// Count bytes/packets sent in last second
|
|
841
|
+
var bytesSentLastSec = 0;
|
|
842
|
+
var packetsSentLastSec = 0;
|
|
843
|
+
for (var i = 0; i < context.sending_app_pn_history.length; i++) {
|
|
844
|
+
if (context.sending_app_pn_history[i][0] > oneSecAgo) {
|
|
845
|
+
bytesSentLastSec += context.sending_app_pn_history[i][1];
|
|
846
|
+
packetsSentLastSec++;
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Prune old history (older than 2 seconds)
|
|
851
|
+
var twoSecAgo = now - 2000;
|
|
852
|
+
while (context.sending_app_pn_history.length > 0 && context.sending_app_pn_history[0][0] < twoSecAgo) {
|
|
853
|
+
context.sending_app_pn_history.shift();
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Rate limits
|
|
857
|
+
var bytesRemaining = context.max_bytes_per_sec - bytesSentLastSec;
|
|
858
|
+
var packetsRemaining = context.max_packets_per_sec - packetsSentLastSec;
|
|
859
|
+
if (bytesRemaining < 0) bytesRemaining = 0;
|
|
860
|
+
if (packetsRemaining < 0) packetsRemaining = 0;
|
|
861
|
+
|
|
862
|
+
// In-flight limits
|
|
863
|
+
var inflightCount = context.sending_app_pn_in_flight.size;
|
|
864
|
+
var inflightRoom = context.max_packets_in_flight - inflightCount;
|
|
865
|
+
if (inflightRoom < 0) inflightRoom = 0;
|
|
866
|
+
|
|
867
|
+
// Calculate burst size
|
|
868
|
+
var burstCount = Math.min(
|
|
869
|
+
context.max_packets_per_burst,
|
|
870
|
+
packetsRemaining,
|
|
871
|
+
inflightRoom,
|
|
872
|
+
Math.floor(bytesRemaining / Math.max(1, 35)) // min packet ~35 bytes
|
|
873
|
+
);
|
|
874
|
+
if (burstCount < 0) burstCount = 0;
|
|
875
|
+
|
|
876
|
+
// Check if there's anything to send
|
|
877
|
+
var hasData = false;
|
|
878
|
+
for (var sid in context.send_streams) {
|
|
879
|
+
var st = context.send_streams[sid];
|
|
880
|
+
if (!st.pending_data || st.pending_data.byteLength === 0) continue;
|
|
881
|
+
var total = st.total_size > 0 ? st.total_size : st.write_offset;
|
|
882
|
+
var known = st.acked_ranges.slice();
|
|
883
|
+
if (st.in_flight_ranges) {
|
|
884
|
+
for (var pn in st.in_flight_ranges) flat_ranges.add(known, st.in_flight_ranges[pn]);
|
|
885
|
+
}
|
|
886
|
+
var missing = flat_ranges.invert(known, 0, total);
|
|
887
|
+
if (missing.length > 0) { hasData = true; break; }
|
|
888
|
+
// Also check: all data sent but FIN not yet sent?
|
|
889
|
+
if (st.total_size > 0 && !st.fin_sent && missing.length === 0) { hasData = true; break; }
|
|
890
|
+
}
|
|
891
|
+
// Also check pending ACK
|
|
892
|
+
var hasPendingAck = context.pending_ack.app.length > 0;
|
|
893
|
+
|
|
894
|
+
if (!hasData && !hasPendingAck) return;
|
|
895
|
+
|
|
896
|
+
// Execute burst
|
|
897
|
+
var sent = false;
|
|
898
|
+
if (burstCount > 0) {
|
|
899
|
+
sent = execute_quic_burst(burstCount);
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Schedule next burst if needed
|
|
903
|
+
if (sent && hasData) {
|
|
904
|
+
// More data to send — schedule next burst immediately (no timer delay)
|
|
905
|
+
context.burst_timer = setImmediate(function () {
|
|
906
|
+
context.burst_timer = null;
|
|
907
|
+
plan_quic_burst();
|
|
908
|
+
});
|
|
909
|
+
} else if (burstCount === 0 && hasData) {
|
|
910
|
+
// Rate limited — wait for capacity
|
|
911
|
+
var waitMs = (packetsRemaining <= 0 || bytesRemaining < 35) ? 50 : 10;
|
|
912
|
+
context.burst_timer = setTimeout(function () {
|
|
913
|
+
context.burst_timer = null;
|
|
914
|
+
plan_quic_burst();
|
|
915
|
+
}, waitMs);
|
|
916
|
+
if (context.burst_timer.unref) context.burst_timer.unref();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* execute_quic_burst — fill up to packet_count packets.
|
|
923
|
+
* Round-robin across active streams.
|
|
924
|
+
* Returns true if at least one packet was sent.
|
|
925
|
+
*/
|
|
926
|
+
function execute_quic_burst(packetCount) {
|
|
927
|
+
var MAX_PAYLOAD = context.max_packet_payload;
|
|
928
|
+
var OVERHEAD = 24; // STREAM frame header overhead estimate
|
|
929
|
+
var sentAny = false;
|
|
930
|
+
|
|
931
|
+
function getActiveIds() {
|
|
932
|
+
var ids = [];
|
|
933
|
+
for (var sid in context.send_streams) {
|
|
934
|
+
var st = context.send_streams[sid];
|
|
935
|
+
if (!st.pending_data || st.pending_data.byteLength === 0) continue;
|
|
936
|
+
var total = st.total_size > 0 ? st.total_size : st.write_offset;
|
|
937
|
+
// Build known_sent = acked + in_flight
|
|
938
|
+
var known = st.acked_ranges.slice();
|
|
939
|
+
if (st.in_flight_ranges) {
|
|
940
|
+
for (var pn in st.in_flight_ranges) flat_ranges.add(known, st.in_flight_ranges[pn]);
|
|
941
|
+
}
|
|
942
|
+
var missing = flat_ranges.invert(known, 0, total);
|
|
943
|
+
if (missing.length > 0) ids.push(Number(sid));
|
|
944
|
+
}
|
|
945
|
+
return ids;
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
var activeIds = getActiveIds();
|
|
949
|
+
var burstYielded = {}; // track ranges yielded per-stream within this burst
|
|
950
|
+
|
|
951
|
+
for (var p = 0; p < packetCount; p++) {
|
|
952
|
+
var frames = [];
|
|
953
|
+
var used = 0;
|
|
954
|
+
|
|
955
|
+
// (1) ACK in first packet if pending
|
|
956
|
+
if (p === 0 && context.pending_ack.app.length > 0) {
|
|
957
|
+
var ackFrame = ranges_to_ack_frame(context.pending_ack.app, null, 0);
|
|
958
|
+
if (ackFrame) {
|
|
959
|
+
var ackEncoded = encode_quic_frames([ackFrame]);
|
|
960
|
+
if (ackEncoded.byteLength <= MAX_PAYLOAD) {
|
|
961
|
+
frames.push(ackFrame);
|
|
962
|
+
used += ackEncoded.byteLength;
|
|
963
|
+
context.pending_ack.app = [];
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// (2) Round-robin STREAM data across active streams
|
|
969
|
+
if (activeIds.length > 0 && used < MAX_PAYLOAD) {
|
|
970
|
+
var progress = true;
|
|
971
|
+
while (used < MAX_PAYLOAD && progress) {
|
|
972
|
+
progress = false;
|
|
973
|
+
|
|
974
|
+
for (var i = 0; i < activeIds.length; i++) {
|
|
975
|
+
if (used >= MAX_PAYLOAD) break;
|
|
976
|
+
|
|
977
|
+
var sid = activeIds[i];
|
|
978
|
+
var st = context.send_streams[sid];
|
|
979
|
+
if (!st || !st.pending_data) continue;
|
|
980
|
+
|
|
981
|
+
var budget = Math.max(0, MAX_PAYLOAD - used - OVERHEAD);
|
|
982
|
+
if (budget <= 0) break;
|
|
983
|
+
|
|
984
|
+
var chunks = get_stream_chunks(sid, budget);
|
|
985
|
+
if (!chunks || chunks.length === 0) continue;
|
|
986
|
+
|
|
987
|
+
for (var c = 0; c < chunks.length; c++) {
|
|
988
|
+
var ch = chunks[c];
|
|
989
|
+
var fin = (st.total_size > 0 && ch.offset + ch.data.byteLength >= st.total_size);
|
|
990
|
+
frames.push({ type: 'stream', id: sid, offset: ch.offset, fin: fin, data: ch.data });
|
|
991
|
+
used += ch.data.byteLength + OVERHEAD;
|
|
992
|
+
progress = true;
|
|
993
|
+
|
|
994
|
+
// Immediately track as yielded so next round-robin iteration won't resend
|
|
995
|
+
if (!burstYielded[sid]) burstYielded[sid] = [];
|
|
996
|
+
flat_ranges.add(burstYielded[sid], [ch.offset, ch.offset + ch.data.byteLength]);
|
|
997
|
+
// Also add to in_flight with temp key so get_stream_chunks sees it
|
|
998
|
+
if (!st.in_flight_ranges) st.in_flight_ranges = {};
|
|
999
|
+
if (!st.in_flight_ranges['_burst']) st.in_flight_ranges['_burst'] = [];
|
|
1000
|
+
flat_ranges.add(st.in_flight_ranges['_burst'], [ch.offset, ch.offset + ch.data.byteLength]);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// (3) FIN-only frames: if all data sent/in-flight but FIN not yet sent
|
|
1007
|
+
for (var sid in context.send_streams) {
|
|
1008
|
+
var st = context.send_streams[sid];
|
|
1009
|
+
if (!st || st.total_size <= 0 || st.fin_sent) continue;
|
|
1010
|
+
// Check if all bytes are covered (acked + in_flight)
|
|
1011
|
+
var known = st.acked_ranges.slice();
|
|
1012
|
+
if (st.in_flight_ranges) {
|
|
1013
|
+
for (var pn in st.in_flight_ranges) flat_ranges.add(known, st.in_flight_ranges[pn]);
|
|
1014
|
+
}
|
|
1015
|
+
var missing = flat_ranges.invert(known, 0, st.total_size);
|
|
1016
|
+
console.log('[quic] FIN-only check stream=' + sid + ' total_size=' + st.total_size + ' fin_sent=' + st.fin_sent + ' missing=' + missing.length + ' known=' + JSON.stringify(known));
|
|
1017
|
+
if (missing.length === 0) {
|
|
1018
|
+
console.log('[quic] → sending FIN-only for stream ' + sid);
|
|
1019
|
+
// All data sent — send FIN-only STREAM frame
|
|
1020
|
+
frames.push({ type: 'stream', id: Number(sid), offset: st.total_size, fin: true, data: new Uint8Array(0) });
|
|
1021
|
+
st.fin_sent = true;
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Send packet if it has content beyond just ACK-only
|
|
1026
|
+
if (frames.length > 0) {
|
|
1027
|
+
sendFrames('app', frames);
|
|
1028
|
+
sentAny = true;
|
|
1029
|
+
// Clean up temp _burst in_flight keys (real PN keys now recorded by sendFrames)
|
|
1030
|
+
for (var sid in burstYielded) {
|
|
1031
|
+
var st = context.send_streams[sid];
|
|
1032
|
+
if (st && st.in_flight_ranges && st.in_flight_ranges['_burst']) {
|
|
1033
|
+
delete st.in_flight_ranges['_burst'];
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
} else {
|
|
1037
|
+
break; // nothing to fill further packets
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Refresh active list
|
|
1041
|
+
activeIds = getActiveIds();
|
|
1042
|
+
if (activeIds.length === 0) break;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
return sentAny;
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* get_stream_chunks — find what bytes are missing (not acked) and
|
|
1051
|
+
* extract up to maxBytes from pending_data.
|
|
1052
|
+
* Uses flat_ranges.invert(acked_ranges) — the heart of retransmit.
|
|
1053
|
+
* Adapted from old get_stream_chunks_to_send().
|
|
1054
|
+
*
|
|
1055
|
+
* Strategy: first forward from send_offset (new data),
|
|
1056
|
+
* then fill gaps before send_offset (retransmit).
|
|
1057
|
+
* pending_data is NEVER deleted — only when stream is fully acked.
|
|
1058
|
+
*/
|
|
1059
|
+
function get_stream_chunks(streamId, maxBytes) {
|
|
1060
|
+
var stream = context.send_streams[streamId];
|
|
1061
|
+
if (!stream || !stream.pending_data || stream.pending_data.byteLength === 0) return [];
|
|
1062
|
+
|
|
1063
|
+
// Flow control: cap by remaining remote_max_data budget
|
|
1064
|
+
var fcBudget = context.remote_max_data - context.bytes_sent;
|
|
1065
|
+
if (fcBudget <= 0) return []; // blocked by flow control
|
|
1066
|
+
maxBytes = Math.min(maxBytes, fcBudget);
|
|
1067
|
+
|
|
1068
|
+
var data = stream.pending_data;
|
|
1069
|
+
var baseOffset = stream.pending_offset_start;
|
|
1070
|
+
var totalSize = stream.total_size > 0 ? stream.total_size : stream.write_offset;
|
|
1071
|
+
|
|
1072
|
+
// Build "known sent" = acked + in_flight (don't resend what's already in transit)
|
|
1073
|
+
var knownSent = stream.acked_ranges.slice(); // copy
|
|
1074
|
+
if (stream.in_flight_ranges) {
|
|
1075
|
+
for (var pn in stream.in_flight_ranges) {
|
|
1076
|
+
flat_ranges.add(knownSent, stream.in_flight_ranges[pn]);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
// What truly needs sending? = invert of knownSent over [0, totalSize)
|
|
1081
|
+
var missing = flat_ranges.invert(knownSent, 0, totalSize);
|
|
1082
|
+
if (!missing || missing.length === 0) return [];
|
|
1083
|
+
|
|
1084
|
+
var sendOffset = stream.send_offset;
|
|
1085
|
+
var chunks = [];
|
|
1086
|
+
var used = 0;
|
|
1087
|
+
|
|
1088
|
+
// Phase 1: forward from send_offset (new data, priority)
|
|
1089
|
+
for (var i = 0; i < missing.length; i += 2) {
|
|
1090
|
+
if (used >= maxBytes) break;
|
|
1091
|
+
var mFrom = missing[i];
|
|
1092
|
+
var mTo = missing[i + 1];
|
|
1093
|
+
|
|
1094
|
+
// Only ranges at or after send_offset
|
|
1095
|
+
if (mTo <= sendOffset) continue;
|
|
1096
|
+
var from = Math.max(mFrom, sendOffset);
|
|
1097
|
+
var to = mTo;
|
|
1098
|
+
|
|
1099
|
+
// Clamp to budget
|
|
1100
|
+
var len = Math.min(to - from, maxBytes - used);
|
|
1101
|
+
if (len <= 0) continue;
|
|
1102
|
+
|
|
1103
|
+
// Clamp to buffer bounds
|
|
1104
|
+
var relStart = from - baseOffset;
|
|
1105
|
+
var relEnd = relStart + len;
|
|
1106
|
+
if (relStart < 0 || relStart >= data.byteLength) continue;
|
|
1107
|
+
if (relEnd > data.byteLength) { relEnd = data.byteLength; len = relEnd - relStart; }
|
|
1108
|
+
if (len <= 0) continue;
|
|
1109
|
+
|
|
1110
|
+
chunks.push({ offset: from, data: data.slice(relStart, relEnd) });
|
|
1111
|
+
used += len;
|
|
1112
|
+
|
|
1113
|
+
// Advance send_offset
|
|
1114
|
+
if (from + len > stream.send_offset) {
|
|
1115
|
+
stream.send_offset = from + len;
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
// Phase 2: fill gaps before send_offset (retransmit lost data)
|
|
1120
|
+
// First update knownSent with what Phase 1 just yielded
|
|
1121
|
+
if (chunks.length > 0) {
|
|
1122
|
+
for (var ci = 0; ci < chunks.length; ci++) {
|
|
1123
|
+
flat_ranges.add(knownSent, [chunks[ci].offset, chunks[ci].offset + chunks[ci].data.byteLength]);
|
|
1124
|
+
}
|
|
1125
|
+
missing = flat_ranges.invert(knownSent, 0, totalSize);
|
|
1126
|
+
}
|
|
1127
|
+
if (used < maxBytes) {
|
|
1128
|
+
for (var i = 0; i < missing.length; i += 2) {
|
|
1129
|
+
if (used >= maxBytes) break;
|
|
1130
|
+
var mFrom = missing[i];
|
|
1131
|
+
var mTo = missing[i + 1];
|
|
1132
|
+
|
|
1133
|
+
// Only ranges before send_offset
|
|
1134
|
+
if (mFrom >= sendOffset) break;
|
|
1135
|
+
var to = Math.min(mTo, sendOffset);
|
|
1136
|
+
|
|
1137
|
+
var len = Math.min(to - mFrom, maxBytes - used);
|
|
1138
|
+
if (len <= 0) continue;
|
|
1139
|
+
|
|
1140
|
+
var relStart = mFrom - baseOffset;
|
|
1141
|
+
var relEnd = relStart + len;
|
|
1142
|
+
if (relStart < 0 || relStart >= data.byteLength) continue;
|
|
1143
|
+
if (relEnd > data.byteLength) { relEnd = data.byteLength; len = relEnd - relStart; }
|
|
1144
|
+
if (len <= 0) continue;
|
|
1145
|
+
|
|
1146
|
+
chunks.push({ offset: mFrom, data: data.slice(relStart, relEnd) });
|
|
1147
|
+
used += len;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// Trim front of pending_data if beginning is fully acked (optimization)
|
|
1152
|
+
if (stream.acked_ranges.length >= 2 && stream.acked_ranges[0] === 0 && stream.acked_ranges[1] > baseOffset) {
|
|
1153
|
+
var trimTo = stream.acked_ranges[1];
|
|
1154
|
+
var trimBytes = trimTo - baseOffset;
|
|
1155
|
+
if (trimBytes > 0 && trimBytes < data.byteLength) {
|
|
1156
|
+
stream.pending_data = data.slice(trimBytes);
|
|
1157
|
+
stream.pending_offset_start = trimTo;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
return chunks;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
|
|
1165
|
+
// ============================================================
|
|
1166
|
+
// Close
|
|
1167
|
+
// ============================================================
|
|
1168
|
+
|
|
1169
|
+
function close(errorCode, reason) {
|
|
1170
|
+
if (context.state === 'closed' || context.state === 'draining') return;
|
|
1171
|
+
if (context.handshake_timer) { clearTimeout(context.handshake_timer); context.handshake_timer = null; }
|
|
1172
|
+
sendFrames('app', [{
|
|
1173
|
+
type: 'connection_close', application: false,
|
|
1174
|
+
error: errorCode || 0, frameType: 0, reason: reason || ''
|
|
1175
|
+
}]);
|
|
1176
|
+
set_context({ state: 'draining' });
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
|
|
1180
|
+
// ============================================================
|
|
1181
|
+
// Client connect (Phase 7)
|
|
1182
|
+
// ============================================================
|
|
1183
|
+
|
|
1184
|
+
function connect() {
|
|
1185
|
+
if (context.isServer) return; // server waits for feedPacket, doesn't connect
|
|
1186
|
+
|
|
1187
|
+
// Generate random DCID (8 bytes)
|
|
1188
|
+
var dcid = new Uint8Array(8);
|
|
1189
|
+
for (var i = 0; i < 8; i++) dcid[i] = Math.floor(Math.random() * 256);
|
|
1190
|
+
|
|
1191
|
+
// Generate random SCID (8 bytes)
|
|
1192
|
+
var scid = new Uint8Array(8);
|
|
1193
|
+
for (var i = 0; i < 8; i++) scid[i] = Math.floor(Math.random() * 256);
|
|
1194
|
+
|
|
1195
|
+
context.original_dcid = dcid;
|
|
1196
|
+
context.my_cids.push(scid);
|
|
1197
|
+
|
|
1198
|
+
// Client initial keys: SWAPPED from server
|
|
1199
|
+
// Client writes with "client in" keys (direction='read' in our function)
|
|
1200
|
+
// Client reads with "server in" keys (direction='write' in our function)
|
|
1201
|
+
set_context({
|
|
1202
|
+
initial_write: quic_derive_init_secrets(dcid, context.version, 'read'),
|
|
1203
|
+
initial_read: quic_derive_init_secrets(dcid, context.version, 'write')
|
|
1204
|
+
});
|
|
1205
|
+
|
|
1206
|
+
// Initialize TLS (client mode)
|
|
1207
|
+
console.log('[quic] client connecting, dcid=' + Array.from(dcid).map(function(b){ return b.toString(16).padStart(2,'0'); }).join(''));
|
|
1208
|
+
initTLS();
|
|
1209
|
+
|
|
1210
|
+
// Trigger ClientHello by feeding empty message
|
|
1211
|
+
tls.feedMessage(new Uint8Array(0));
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
// ============================================================
|
|
1216
|
+
// Public API
|
|
1217
|
+
// ============================================================
|
|
1218
|
+
|
|
1219
|
+
var api = {
|
|
1220
|
+
context: context,
|
|
1221
|
+
on: function (name, fn) { ev.on(name, fn); },
|
|
1222
|
+
off: function (name, fn) { ev.off(name, fn); },
|
|
1223
|
+
feedDatagram: feedDatagram,
|
|
1224
|
+
sendStream: sendStream,
|
|
1225
|
+
sendDatagram: sendDatagram,
|
|
1226
|
+
close: close,
|
|
1227
|
+
connect: connect,
|
|
1228
|
+
set_context: set_context,
|
|
1229
|
+
get state() { return context.state; }
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
for (var k in api) {
|
|
1233
|
+
if (Object.prototype.hasOwnProperty.call(api, k)) this[k] = api[k];
|
|
1234
|
+
}
|
|
1235
|
+
return this;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
export { QUICConnection };
|