quico 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/transport.js CHANGED
@@ -13,6 +13,7 @@
13
13
  */
14
14
 
15
15
  import {
16
+ DEBUG,
16
17
  concatUint8Arrays,
17
18
  writeVarInt,
18
19
  readVarInt
@@ -142,7 +143,7 @@ function parse_quic_datagram(array) {
142
143
  : array.slice(start, end);
143
144
 
144
145
  if (packets.length > 0) {
145
- console.log('[quic] coalesced pkt #' + packets.length + ' type=' + pkt.type + ' offset=' + start + ' len=' + pkt.totalLength + ' firstByte=0x' + array[start].toString(16).padStart(2,'0'));
146
+ if (DEBUG) console.log('[quic] coalesced pkt #' + packets.length + ' type=' + pkt.type + ' offset=' + start + ' len=' + pkt.totalLength + ' firstByte=0x' + array[start].toString(16).padStart(2,'0'));
146
147
  }
147
148
 
148
149
  packets.push(pkt);
@@ -201,18 +202,25 @@ function encode_quic_frames(frames) {
201
202
  } else if (frame.type === 'stream') {
202
203
  var typeByte = 0x08;
203
204
  var hasOffset = (frame.offset != null && frame.offset > 0);
204
- var hasLen = (frame.data && frame.data.length > 0);
205
205
  var hasFin = !!frame.fin;
206
-
206
+ var dataLen = (frame.data && frame.data.length) ? frame.data.length : 0;
207
+
208
+ // ALWAYS emit the LEN field. A STREAM frame without the LEN bit means
209
+ // "data runs to the end of the packet"; if anything follows it in the
210
+ // packet (another frame — e.g. a zero-length FIN-only batched with
211
+ // others — or padding), the peer reads those trailing bytes as stream
212
+ // data and the offset overshoots the declared final size
213
+ // (FINAL_SIZE / PROTOCOL_VIOLATION 0xa). An explicit length is always
214
+ // safe and self-delimiting.
207
215
  if (hasOffset) typeByte |= 0x04;
208
- if (hasLen) typeByte |= 0x02;
216
+ typeByte |= 0x02; // LEN — always present
209
217
  if (hasFin) typeByte |= 0x01;
210
218
 
211
219
  parts.push(concatUint8Arrays([
212
220
  new Uint8Array([typeByte]),
213
221
  writeVarInt(frame.id),
214
222
  hasOffset ? writeVarInt(frame.offset) : new Uint8Array(0),
215
- hasLen ? writeVarInt(frame.data.length) : new Uint8Array(0),
223
+ writeVarInt(dataLen),
216
224
  frame.data || new Uint8Array(0)
217
225
  ]));
218
226
 
@@ -315,18 +323,14 @@ function encode_quic_frames(frames) {
315
323
  ]));
316
324
 
317
325
  } else if (frame.type === 'datagram') {
318
- if (frame.contextId != null) {
319
- parts.push(concatUint8Arrays([
320
- new Uint8Array([0x31]),
321
- writeVarInt(frame.contextId),
322
- frame.data
323
- ]));
324
- } else {
325
- parts.push(concatUint8Arrays([
326
- new Uint8Array([0x30]),
327
- frame.data
328
- ]));
329
- }
326
+ // Emit the 0x30 form (no Length): the datagram data runs to the end of
327
+ // the packet. QUICO sends each DATAGRAM as the sole/last frame of its
328
+ // packet, so the length-prefixed 0x31 form is not needed here. (The
329
+ // parser above accepts both forms on receive.)
330
+ parts.push(concatUint8Arrays([
331
+ new Uint8Array([0x30]),
332
+ frame.data
333
+ ]));
330
334
  }
331
335
  }
332
336
 
@@ -498,13 +502,21 @@ function parse_quic_frames(buf) {
498
502
  frames.push({ type: 'handshake_done' });
499
503
 
500
504
  } else if (type === 0x30 || type === 0x31) {
501
- var contextId = null;
505
+ // RFC 9221 DATAGRAM frame. The 0x31 form carries an explicit Length
506
+ // varint and the datagram data is exactly that many bytes; the 0x30 form
507
+ // has no Length and the data runs to the end of the packet (so a 0x30
508
+ // DATAGRAM must be the last frame). The optional field is a Length — not
509
+ // a "context ID" (that draft-era concept was removed before RFC 9221).
510
+ var data;
502
511
  if (type === 0x31) {
503
- var cid = safeReadVarInt(); if (!cid) break;
504
- contextId = cid.value;
512
+ var len = safeReadVarInt(); if (!len) break;
513
+ var end = offset + len.value;
514
+ if (end > buf.length) break; // truncated / invalid length
515
+ data = buf.slice(offset, end); offset = end;
516
+ } else {
517
+ data = buf.slice(offset); offset = buf.length;
505
518
  }
506
- var data = buf.slice(offset); offset = buf.length;
507
- frames.push({ type: 'datagram', contextId: contextId, data: data });
519
+ frames.push({ type: 'datagram', data: data });
508
520
 
509
521
  } else {
510
522
  // Unknown frame — stop
@@ -604,24 +616,28 @@ function parse_transport_params(buf, start) {
604
616
 
605
617
  var id = idVar.value;
606
618
 
619
+ // Safe inner varint: a malformed param could carry an empty/short value,
620
+ // in which case readVarInt returns null — never dereference .value blindly.
621
+ function vi(bytes) { var r = readVarInt(bytes, 0); return r ? r.value : 0; }
622
+
607
623
  switch (id) {
608
624
  case 0x00: out.original_destination_connection_id = valueBytes; break;
609
- case 0x01: out.max_idle_timeout = readVarInt(valueBytes, 0).value; break;
625
+ case 0x01: out.max_idle_timeout = vi(valueBytes); break;
610
626
  case 0x02: out.stateless_reset_token = valueBytes; break;
611
- case 0x03: out.max_udp_payload_size = readVarInt(valueBytes, 0).value; break;
612
- case 0x04: out.initial_max_data = readVarInt(valueBytes, 0).value; break;
613
- case 0x05: out.initial_max_stream_data_bidi_local = readVarInt(valueBytes, 0).value; break;
614
- case 0x06: out.initial_max_stream_data_bidi_remote = readVarInt(valueBytes, 0).value; break;
615
- case 0x07: out.initial_max_stream_data_uni = readVarInt(valueBytes, 0).value; break;
616
- case 0x08: out.initial_max_streams_bidi = readVarInt(valueBytes, 0).value; break;
617
- case 0x09: out.initial_max_streams_uni = readVarInt(valueBytes, 0).value; break;
618
- case 0x0a: out.ack_delay_exponent = readVarInt(valueBytes, 0).value; break;
619
- case 0x0b: out.max_ack_delay = readVarInt(valueBytes, 0).value; break;
627
+ case 0x03: out.max_udp_payload_size = vi(valueBytes); break;
628
+ case 0x04: out.initial_max_data = vi(valueBytes); break;
629
+ case 0x05: out.initial_max_stream_data_bidi_local = vi(valueBytes); break;
630
+ case 0x06: out.initial_max_stream_data_bidi_remote = vi(valueBytes); break;
631
+ case 0x07: out.initial_max_stream_data_uni = vi(valueBytes); break;
632
+ case 0x08: out.initial_max_streams_bidi = vi(valueBytes); break;
633
+ case 0x09: out.initial_max_streams_uni = vi(valueBytes); break;
634
+ case 0x0a: out.ack_delay_exponent = vi(valueBytes); break;
635
+ case 0x0b: out.max_ack_delay = vi(valueBytes); break;
620
636
  case 0x0c: out.disable_active_migration = true; break;
621
- case 0x0e: out.active_connection_id_limit = readVarInt(valueBytes, 0).value; break;
637
+ case 0x0e: out.active_connection_id_limit = vi(valueBytes); break;
622
638
  case 0x0f: out.initial_source_connection_id = valueBytes; break;
623
639
  case 0x10: out.retry_source_connection_id = valueBytes; break;
624
- case 0x20: out.max_datagram_frame_size = readVarInt(valueBytes, 0).value; break;
640
+ case 0x20: out.max_datagram_frame_size = vi(valueBytes); break;
625
641
  default:
626
642
  if (!out.unknown) out.unknown = [];
627
643
  out.unknown.push({ id: id, bytes: valueBytes });
package/src/utils.js CHANGED
@@ -13,6 +13,16 @@
13
13
  */
14
14
 
15
15
 
16
+ // ============================================================
17
+ // Debug flag — set QUICO_DEBUG=1 to enable verbose logging.
18
+ // Read once at module load. Call sites guard with `if (DEBUG)`
19
+ // so the (expensive) log-string construction is skipped entirely
20
+ // when debugging is off.
21
+ // ============================================================
22
+
23
+ var DEBUG = !!(typeof process !== 'undefined' && process.env && process.env.QUICO_DEBUG);
24
+
25
+
16
26
  // ============================================================
17
27
  // Emitter — lightweight event emitter (single shared copy)
18
28
  // ============================================================
@@ -124,10 +134,12 @@ function readVarInt(array, offset) {
124
134
  array[offset + 7]
125
135
  ) >>> 0;
126
136
  var full = BigInt(hi) * 4294967296n + BigInt(lo);
127
- if (full <= BigInt(Number.MAX_SAFE_INTEGER)) {
128
- return { value: Number(full), byteLength: 8 };
129
- }
130
- return { value: full, byteLength: 8 };
137
+ // Always return a Number. QUIC fields (lengths, offsets, IDs, limits) never
138
+ // legitimately exceed 2^53, and a malicious 8-byte value that does will be
139
+ // rejected by the parsers' bounds checks anyway. Returning a BigInt here
140
+ // would throw "Cannot mix BigInt and other types" the moment the value is
141
+ // combined with a Number offset during parsing.
142
+ return { value: Number(full), byteLength: 8 };
131
143
  }
132
144
 
133
145
  return null;
@@ -164,6 +176,46 @@ function uint8Equal(a, b) {
164
176
  }
165
177
 
166
178
 
179
+ // ============================================================
180
+ // WebTransport framing helpers (draft-ietf-webtrans-http3 / RFC 9297)
181
+ //
182
+ // Both client (webtransport.js) and server (h3.js) build the same
183
+ // byte layouts, so they live here to avoid duplicating the prefix
184
+ // construction across the stream and datagram send paths.
185
+ // ============================================================
186
+
187
+ /**
188
+ * Build a WebTransport stream header: VarInt(streamType) + VarInt(sessionId) [+ data].
189
+ * streamType is 0x41 for a bidirectional WT stream, 0x54 for a unidirectional one.
190
+ * The header prefixes the FIRST chunk on a WT stream (or a lone FIN frame); pass
191
+ * `data` for "header + payload", omit it for a header-only (e.g. FIN) frame.
192
+ */
193
+ function wt_stream_header(isBidi, sessionId, data) {
194
+ var typePrefix = writeVarInt(isBidi ? 0x41 : 0x54);
195
+ var sessionPrefix = writeVarInt(sessionId);
196
+ var dataLen = (data && data.byteLength) ? data.byteLength : 0;
197
+ var out = new Uint8Array(typePrefix.length + sessionPrefix.length + dataLen);
198
+ out.set(typePrefix, 0);
199
+ out.set(sessionPrefix, typePrefix.length);
200
+ if (dataLen) out.set(data, typePrefix.length + sessionPrefix.length);
201
+ return out;
202
+ }
203
+
204
+ /**
205
+ * Build a WebTransport datagram payload: VarInt(quarter_stream_id) + data.
206
+ * The quarter-stream-id is the session's CONNECT stream id divided by 4
207
+ * (RFC 9297 §2.1). Pass the session stream id; the division is done here.
208
+ */
209
+ function wt_datagram_payload(sessionStreamId, data) {
210
+ var prefix = writeVarInt(Math.floor(sessionStreamId / 4));
211
+ var dataLen = (data && data.byteLength) ? data.byteLength : 0;
212
+ var out = new Uint8Array(prefix.length + dataLen);
213
+ out.set(prefix, 0);
214
+ if (dataLen) out.set(data, prefix.length);
215
+ return out;
216
+ }
217
+
218
+
167
219
  // ============================================================
168
220
  // ACK range helpers (flat [from,to, from,to, ...] format)
169
221
  // ============================================================
@@ -280,11 +332,14 @@ function ranges_to_ack_frame(flatRanges, ecnStats, ackDelay) {
280
332
 
281
333
 
282
334
  export {
335
+ DEBUG,
283
336
  Emitter,
284
337
  writeVarInt,
285
338
  readVarInt,
286
339
  concatUint8Arrays,
287
340
  uint8Equal,
341
+ wt_stream_header,
342
+ wt_datagram_payload,
288
343
  ack_frame_to_ranges,
289
344
  ranges_to_ack_frame
290
345
  };
@@ -14,11 +14,10 @@
14
14
 
15
15
  import { Duplex, Readable, Writable } from 'node:stream';
16
16
  import { EventEmitter } from 'node:events';
17
- import dgram from 'node:dgram';
18
17
  import dns from 'node:dns';
19
- import { QUICConnection } from './quic_connection.js';
20
18
  import { H3Connection } from './h3.js';
21
- import { writeVarInt, readVarInt } from './utils.js';
19
+ import { createQuicClientSocket } from './quic_socket.js';
20
+ import { readVarInt, concatUint8Arrays, wt_stream_header, wt_datagram_payload } from './utils.js';
22
21
 
23
22
 
24
23
  // ============================================================
@@ -101,13 +100,7 @@ class WebTransportBidiStream extends Duplex {
101
100
  // First write on this stream: prepend VarInt(0x41) + VarInt(session_id)
102
101
  if (!this._prefixSent) {
103
102
  this._prefixSent = true;
104
- var typePrefix = writeVarInt(0x41); // WEBTRANSPORT_STREAM bidi type
105
- var sessionPrefix = writeVarInt(this._sessionId);
106
- var combined = new Uint8Array(typePrefix.length + sessionPrefix.length + chunk.byteLength);
107
- combined.set(typePrefix, 0);
108
- combined.set(sessionPrefix, typePrefix.length);
109
- combined.set(chunk, typePrefix.length + sessionPrefix.length);
110
- this._quic.sendStream(this._streamId, combined, false);
103
+ this._quic.sendStream(this._streamId, wt_stream_header(true, this._sessionId, chunk), false);
111
104
  } else {
112
105
  this._quic.sendStream(this._streamId, chunk, false);
113
106
  }
@@ -118,12 +111,7 @@ class WebTransportBidiStream extends Duplex {
118
111
  // Send FIN (with type + session_id prefix if nothing was written yet)
119
112
  if (!this._prefixSent) {
120
113
  this._prefixSent = true;
121
- var typePrefix = writeVarInt(0x41);
122
- var sessionPrefix = writeVarInt(this._sessionId);
123
- var header = new Uint8Array(typePrefix.length + sessionPrefix.length);
124
- header.set(typePrefix, 0);
125
- header.set(sessionPrefix, typePrefix.length);
126
- this._quic.sendStream(this._streamId, header, true);
114
+ this._quic.sendStream(this._streamId, wt_stream_header(true, this._sessionId), true);
127
115
  } else {
128
116
  this._quic.sendStream(this._streamId, new Uint8Array(0), true);
129
117
  }
@@ -168,13 +156,7 @@ class WebTransportUniWriteStream extends Writable {
168
156
  if (!this._headerSent) {
169
157
  this._headerSent = true;
170
158
  // Uni stream: VarInt(0x54) (WEBTRANSPORT_STREAM) + VarInt(session_id) + data
171
- var typePrefix = writeVarInt(0x54);
172
- var sessionPrefix = writeVarInt(this._sessionId);
173
- var header = new Uint8Array(typePrefix.length + sessionPrefix.length + chunk.byteLength);
174
- header.set(typePrefix, 0);
175
- header.set(sessionPrefix, typePrefix.length);
176
- header.set(chunk, typePrefix.length + sessionPrefix.length);
177
- this._quic.sendStream(this._streamId, header, false);
159
+ this._quic.sendStream(this._streamId, wt_stream_header(false, this._sessionId, chunk), false);
178
160
  } else {
179
161
  this._quic.sendStream(this._streamId, chunk, false);
180
162
  }
@@ -184,12 +166,7 @@ class WebTransportUniWriteStream extends Writable {
184
166
  _final(callback) {
185
167
  if (!this._headerSent) {
186
168
  this._headerSent = true;
187
- var typePrefix = writeVarInt(0x54);
188
- var sessionPrefix = writeVarInt(this._sessionId);
189
- var header = new Uint8Array(typePrefix.length + sessionPrefix.length);
190
- header.set(typePrefix, 0);
191
- header.set(sessionPrefix, typePrefix.length);
192
- this._quic.sendStream(this._streamId, header, true);
169
+ this._quic.sendStream(this._streamId, wt_stream_header(false, this._sessionId), true);
193
170
  } else {
194
171
  this._quic.sendStream(this._streamId, new Uint8Array(0), true);
195
172
  }
@@ -350,43 +327,31 @@ class WebTransport extends EventEmitter {
350
327
  _startQuic(remoteIp) {
351
328
  var self = this;
352
329
  var remotePort = this._port;
353
- var isIPv6 = remoteIp.indexOf(':') >= 0;
354
330
 
355
- this._udpSocket = dgram.createSocket(isIPv6 ? 'udp6' : 'udp4');
331
+ this._udpSocket = createQuicClientSocket({
332
+ remoteIp: remoteIp,
333
+ remotePort: remotePort,
334
+ hostname: this._hostname,
356
335
 
357
- this._udpSocket.on('message', function (msg, rinfo) {
358
- if (self._quic) self._quic.feedDatagram(rinfo.address, rinfo.port, new Uint8Array(msg));
359
- });
360
-
361
- this._udpSocket.on('error', function (err) {
362
- if (self._state === 'connecting') self._readyReject(err);
363
- self.emit('error', err);
364
- });
365
-
366
- this._udpSocket.bind(0, function () {
367
- self._quic = new QUICConnection({
368
- isServer: false,
369
- hostname: self._hostname
370
- });
371
-
372
- self._quic.on('packet', function (data) {
373
- self._udpSocket.send(data, remotePort, remoteIp);
374
- });
336
+ onError: function (err) {
337
+ if (self._state === 'connecting') self._readyReject(err);
338
+ self.emit('error', err);
339
+ },
375
340
 
376
- self._quic.on('connect', function () {
341
+ onConnect: function (q, s) {
342
+ self._quic = q;
343
+ self._udpSocket = s;
377
344
  self._h3 = new H3Connection({ quicConnection: self._quic, isServer: false, enableWebTransport: true });
378
345
  self._setupH3Handlers();
379
346
  self._sendConnect();
380
- });
347
+ },
381
348
 
382
- self._quic.on('close', function () {
349
+ onClose: function () {
383
350
  self._state = 'closed';
384
351
  self._closedResolve();
385
352
  self.emit('close');
386
- if (self._udpSocket) { try { self._udpSocket.close(); } catch (e) {} }
387
- });
388
-
389
- self._quic.connect();
353
+ // The helper closes the UDP socket.
354
+ }
390
355
  });
391
356
  }
392
357
 
@@ -424,7 +389,7 @@ class WebTransport extends EventEmitter {
424
389
  });
425
390
 
426
391
  // Datagrams — QUIC DATAGRAM payload = VarInt(quarter_stream_id) + app data (RFC 9297)
427
- this._quic.on('datagram', function (_contextId, rawData) {
392
+ this._quic.on('datagram', function (rawData) {
428
393
  if (!rawData || rawData.byteLength === 0) return;
429
394
 
430
395
  // Always parse VarInt quarter_stream_id from payload
@@ -484,7 +449,7 @@ class WebTransport extends EventEmitter {
484
449
  buf.totalLen += data.byteLength;
485
450
 
486
451
  // Try to parse prefix
487
- var combined = buf.totalLen === data.byteLength ? data : this._combineChunks(buf.chunks, buf.totalLen);
452
+ var combined = buf.totalLen === data.byteLength ? data : concatUint8Arrays(buf.chunks);
488
453
  var isBidi = (streamId & 0x2) === 0;
489
454
  var offset = 0;
490
455
 
@@ -552,18 +517,6 @@ class WebTransport extends EventEmitter {
552
517
  }
553
518
  }
554
519
 
555
- _combineChunks(chunks, totalLen) {
556
- if (chunks.length === 1) return chunks[0];
557
- var out = new Uint8Array(totalLen);
558
- var off = 0;
559
- for (var i = 0; i < chunks.length; i++) {
560
- out.set(chunks[i], off);
561
- off += chunks[i].byteLength;
562
- }
563
- return out;
564
- }
565
-
566
-
567
520
  // ---- Send CONNECT ----
568
521
 
569
522
  _sendConnect() {
@@ -660,14 +613,8 @@ class WebTransport extends EventEmitter {
660
613
  if (typeof data === 'string') data = Buffer.from(data);
661
614
  if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
662
615
 
663
- // Quarter stream ID as context ID (RFC 9297)
664
- var contextId = Math.floor(this._sessionId / 4);
665
-
666
- // Build datagram: VarInt(contextId) + payload
667
- var contextPrefix = writeVarInt(contextId);
668
- var frame = new Uint8Array(contextPrefix.length + data.byteLength);
669
- frame.set(contextPrefix, 0);
670
- frame.set(data, contextPrefix.length);
616
+ // Build datagram: VarInt(quarter_stream_id) + payload (RFC 9297)
617
+ var frame = wt_datagram_payload(this._sessionId, data);
671
618
 
672
619
  // Send via QUIC DATAGRAM frame
673
620
  if (this._quic && this._quic.sendDatagram) {