quico 0.7.0 → 0.9.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 +247 -33
- package/index.d.ts +1034 -77
- package/index.js +7 -3
- package/package.json +9 -5
- package/src/client.js +1 -0
- package/src/connect_udp.js +5 -2
- package/src/connect_udp_client.js +10 -0
- package/src/crypto.js +6 -0
- package/src/h1_server.js +51 -9
- package/src/h2_server.js +60 -11
- package/src/h3.js +115 -70
- package/src/h3_client.js +25 -5
- package/src/h3_server.js +104 -6
- package/src/hpack_primitives.js +64 -25
- package/src/http_common.js +7 -0
- package/src/qpack.js +11 -2
- package/src/quic_connection.js +379 -63
- package/src/quic_server.js +213 -72
- package/src/quic_socket.js +5 -5
- package/src/server.js +277 -24
- package/src/streams.js +183 -10
- package/src/tcp_listener.js +11 -0
- package/src/tls_bridge.js +62 -38
- package/src/transport.js +6 -2
- package/src/utils.js +36 -0
- package/src/webtransport.js +432 -57
- package/src/ws.js +120 -0
- package/src/ws_client.js +68 -88
- package/src/ws_server.js +35 -53
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +533 -76
package/src/h3.js
CHANGED
|
@@ -87,8 +87,12 @@ import {
|
|
|
87
87
|
// deliberately NOT part of this move.
|
|
88
88
|
// ============================================================
|
|
89
89
|
|
|
90
|
+
// One decoder for the module: decode() holds no state between calls (no
|
|
91
|
+
// `stream` option), and constructing one per name and per value cost more
|
|
92
|
+
// than the decoding itself.
|
|
93
|
+
var _utf8Decoder = new TextDecoder();
|
|
90
94
|
function _text(bytes) {
|
|
91
|
-
return
|
|
95
|
+
return _utf8Decoder.decode(bytes);
|
|
92
96
|
}
|
|
93
97
|
|
|
94
98
|
// Every decodeStringLiteral() call below passes strict = true, enabling the
|
|
@@ -249,7 +253,10 @@ function parseQpackHeaderBlock(buf, dynamicTable, totalInserts, maxEntries, maxS
|
|
|
249
253
|
|
|
250
254
|
|
|
251
255
|
// ============================================================
|
|
252
|
-
// H3 Frame build
|
|
256
|
+
// H3 Frame build
|
|
257
|
+
//
|
|
258
|
+
// Frames are read in place, from each stream's own buffer, as the bytes
|
|
259
|
+
// arrive: processBidiStream and processControlStream in H3Connection.
|
|
253
260
|
// ============================================================
|
|
254
261
|
|
|
255
262
|
function buildH3Frames(frames) {
|
|
@@ -262,43 +269,6 @@ function buildH3Frames(frames) {
|
|
|
262
269
|
return concatUint8Arrays(parts);
|
|
263
270
|
}
|
|
264
271
|
|
|
265
|
-
function extractH3Frames(chunks, fromOffset) {
|
|
266
|
-
if (!chunks || chunks.length === 0) return { frames: [], new_from_offset: fromOffset };
|
|
267
|
-
|
|
268
|
-
var buffers = [];
|
|
269
|
-
var acc = 0;
|
|
270
|
-
for (var i = 0; i < chunks.length; i++) {
|
|
271
|
-
var nextAcc = acc + chunks[i].length;
|
|
272
|
-
if (fromOffset < nextAcc) {
|
|
273
|
-
buffers.push(chunks[i].slice(fromOffset - acc));
|
|
274
|
-
for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
|
|
275
|
-
break;
|
|
276
|
-
}
|
|
277
|
-
acc = nextAcc;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (buffers.length === 0) return { frames: [], new_from_offset: fromOffset };
|
|
281
|
-
|
|
282
|
-
var combined = concatUint8Arrays(buffers);
|
|
283
|
-
var offset = 0, frames = [];
|
|
284
|
-
|
|
285
|
-
while (offset < combined.length) {
|
|
286
|
-
var start = offset;
|
|
287
|
-
var ft = readVarInt(combined, offset);
|
|
288
|
-
if (!ft) break;
|
|
289
|
-
offset += ft.byteLength;
|
|
290
|
-
var len = readVarInt(combined, offset);
|
|
291
|
-
if (!len) { offset = start; break; }
|
|
292
|
-
offset += len.byteLength;
|
|
293
|
-
if (offset + len.value > combined.length) { offset = start; break; }
|
|
294
|
-
frames.push({ frame_type: ft.value, payload: combined.slice(offset, offset + len.value) });
|
|
295
|
-
offset += len.value;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return { frames: frames, new_from_offset: fromOffset + offset };
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
|
|
302
272
|
// ============================================================
|
|
303
273
|
// H3 Settings
|
|
304
274
|
// ============================================================
|
|
@@ -337,8 +307,17 @@ function parseSettingsPayload(buf) {
|
|
|
337
307
|
var list = [];
|
|
338
308
|
var seen = {};
|
|
339
309
|
while (offset < buf.length) {
|
|
340
|
-
|
|
341
|
-
|
|
310
|
+
// A payload that ends inside an identifier or before its value is
|
|
311
|
+
// H3_FRAME_ERROR (RFC 9114 §7.1). It was cut short silently: the setting
|
|
312
|
+
// dropped, the connection on.
|
|
313
|
+
var id = readVarInt(buf, offset);
|
|
314
|
+
var val = id ? readVarInt(buf, offset + id.byteLength) : null;
|
|
315
|
+
if (!id || !val) {
|
|
316
|
+
var te = new Error('SETTINGS payload ends inside a setting (' + (buf.length - offset) + ' byte(s) left)');
|
|
317
|
+
te.h3ErrorCode = 0x0106;
|
|
318
|
+
throw te;
|
|
319
|
+
}
|
|
320
|
+
offset += id.byteLength + val.byteLength;
|
|
342
321
|
// RFC 9114 §7.2.4: an identifier twice, or one of HTTP/2's that HTTP/3
|
|
343
322
|
// reserves (0x02-0x05), is H3_SETTINGS_ERROR.
|
|
344
323
|
if (seen[id.value] || (id.value >= 0x02 && id.value <= 0x05)) {
|
|
@@ -369,24 +348,14 @@ function parseSettingsPayload(buf) {
|
|
|
369
348
|
// QPACK Encoder Instructions (from peer's encoder stream)
|
|
370
349
|
// ============================================================
|
|
371
350
|
|
|
372
|
-
|
|
373
|
-
|
|
351
|
+
// Parse the encoder-stream instructions in `combined` — the stream's unparsed
|
|
352
|
+
// bytes, from its read position. Returns the complete instructions, how many
|
|
353
|
+
// bytes they took (consumed), and, when the last one is cut short, how many
|
|
354
|
+
// bytes it needs (need). It used to take the stream's chunks and an offset
|
|
355
|
+
// and join them itself; the one caller already hands it a single buffer.
|
|
356
|
+
function parseQpackEncoderInstructions(combined, maxLiteral) {
|
|
357
|
+
if (!combined || combined.length === 0) return { instructions: [], consumed: 0, need: 0 };
|
|
374
358
|
if (maxLiteral === undefined) maxLiteral = Infinity;
|
|
375
|
-
|
|
376
|
-
var buffers = [];
|
|
377
|
-
var acc = 0;
|
|
378
|
-
for (var i = 0; i < chunks.length; i++) {
|
|
379
|
-
var nextAcc = acc + chunks[i].length;
|
|
380
|
-
if (fromOffset < nextAcc) {
|
|
381
|
-
buffers.push(chunks[i].slice(fromOffset - acc));
|
|
382
|
-
for (var j = i + 1; j < chunks.length; j++) buffers.push(chunks[j]);
|
|
383
|
-
break;
|
|
384
|
-
}
|
|
385
|
-
acc = nextAcc;
|
|
386
|
-
}
|
|
387
|
-
if (buffers.length === 0) return { instructions: [], new_from_offset: fromOffset, need: 0 };
|
|
388
|
-
|
|
389
|
-
var combined = buffers.length === 1 ? buffers[0] : concatUint8Arrays(buffers);
|
|
390
359
|
var pos = 0;
|
|
391
360
|
var instructions = [];
|
|
392
361
|
// When the last instruction is cut short: how many bytes, from its start,
|
|
@@ -457,7 +426,7 @@ function parseQpackEncoderInstructions(chunks, fromOffset, maxLiteral) {
|
|
|
457
426
|
}
|
|
458
427
|
}
|
|
459
428
|
|
|
460
|
-
return { instructions: instructions,
|
|
429
|
+
return { instructions: instructions, consumed: pos,
|
|
461
430
|
need: needAbs > pos ? needAbs - pos : 0 };
|
|
462
431
|
}
|
|
463
432
|
|
|
@@ -686,6 +655,11 @@ function H3Connection(options) {
|
|
|
686
655
|
var decoderStreamId = null;
|
|
687
656
|
var qpackKnownReceived = 0; // insertions the peer has acknowledged
|
|
688
657
|
var qpackPendingAcks = 0; // insertions of THEIRS we owe an ack for
|
|
658
|
+
// Field sections we sent with a non-zero Required Insert Count, not yet
|
|
659
|
+
// acknowledged: stream id → [RIC, …] in the order sent (RFC 9204 §2.1.1).
|
|
660
|
+
// The decoder acknowledges each (Section Acknowledgment), or cancels the
|
|
661
|
+
// stream's; an acknowledgment for none is an error (§4.4.1).
|
|
662
|
+
var qpackUnacked = new Map();
|
|
689
663
|
|
|
690
664
|
// ---- SETTINGS remembered for 0-RTT (RFC 9114 §7.2.4.2) ----
|
|
691
665
|
//
|
|
@@ -784,9 +758,10 @@ function H3Connection(options) {
|
|
|
784
758
|
* reference — and until an entry is below it, we send a literal instead.
|
|
785
759
|
* That single number is what makes this encoder non-blocking.
|
|
786
760
|
*
|
|
787
|
-
*
|
|
788
|
-
*
|
|
789
|
-
*
|
|
761
|
+
* Each instruction is held to what we actually sent (RFC 9204 §4.4): a
|
|
762
|
+
* Section Acknowledgment for a section we have outstanding on that stream,
|
|
763
|
+
* an Insert Count Increment of at least 1 that does not pass the entries we
|
|
764
|
+
* inserted. Anything else is QPACK_DECODER_STREAM_ERROR.
|
|
790
765
|
*/
|
|
791
766
|
function processQpackDecoderStream(s) {
|
|
792
767
|
if (s.buffered === 0) return;
|
|
@@ -795,14 +770,41 @@ function H3Connection(options) {
|
|
|
795
770
|
var buf = Buffer.from(copyBytes(s, s.buffered));
|
|
796
771
|
|
|
797
772
|
var r = qpackParseDecoderInstructions(buf, 0);
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
if (
|
|
801
|
-
|
|
773
|
+
for (var k = 0; k < r.instructions.length; k++) {
|
|
774
|
+
var ins = r.instructions[k];
|
|
775
|
+
if (ins.type === 'section_ack') {
|
|
776
|
+
var pend = qpackUnacked.get(ins.value);
|
|
777
|
+
if (!pend || pend.length === 0) {
|
|
778
|
+
throw qpackDecoderStreamError('Section Acknowledgment for stream ' + ins.value +
|
|
779
|
+
', which has no field section awaiting one');
|
|
780
|
+
}
|
|
781
|
+
var ric = pend.shift();
|
|
782
|
+
if (pend.length === 0) qpackUnacked.delete(ins.value);
|
|
783
|
+
// §4.4.1: the acknowledged section's entries are now known received.
|
|
784
|
+
if (ric > qpackKnownReceived) qpackKnownReceived = ric;
|
|
785
|
+
} else if (ins.type === 'stream_cancel') {
|
|
786
|
+
qpackUnacked.delete(ins.value);
|
|
787
|
+
} else {
|
|
788
|
+
// §4.4.3: zero, or past what was inserted, is an error.
|
|
789
|
+
if (ins.value === 0) throw qpackDecoderStreamError('Insert Count Increment of 0');
|
|
790
|
+
if (qpackKnownReceived + ins.value > encoderTable.insertCount) {
|
|
791
|
+
throw qpackDecoderStreamError('Insert Count Increment to ' + (qpackKnownReceived + ins.value) +
|
|
792
|
+
', beyond the ' + encoderTable.insertCount + ' entries inserted');
|
|
793
|
+
}
|
|
794
|
+
qpackKnownReceived += ins.value;
|
|
795
|
+
}
|
|
802
796
|
}
|
|
797
|
+
if (DEBUG && r.instructions.length) console.log('[h3] qpack decoder instructions ' + r.instructions.length +
|
|
798
|
+
' (known=' + qpackKnownReceived + ')');
|
|
803
799
|
consumeBytes(s, r.consumed);
|
|
804
800
|
}
|
|
805
801
|
|
|
802
|
+
function qpackDecoderStreamError(msg) {
|
|
803
|
+
var err = new Error('QPACK decoder stream: ' + msg);
|
|
804
|
+
err.h3ErrorCode = H3_ERR.QPACK_DECODER_STREAM_ERROR;
|
|
805
|
+
return err;
|
|
806
|
+
}
|
|
807
|
+
|
|
806
808
|
function evictQpackTable(limit) {
|
|
807
809
|
// Oldest first, until what remains fits. Sizes in BYTES: measuring by
|
|
808
810
|
// string length, as this did, undercounts any non-ASCII header value and
|
|
@@ -887,7 +889,15 @@ function H3Connection(options) {
|
|
|
887
889
|
if (context.control_streams_sent) return;
|
|
888
890
|
context.control_streams_sent = true;
|
|
889
891
|
|
|
890
|
-
|
|
892
|
+
// A reserved identifier among them, of a random N (RFC 9114 §7.2.4.1,
|
|
893
|
+
// 0x1f·N + 0x21): peers must ignore it — sending one keeps them honest.
|
|
894
|
+
// Not part of localSettings: 0-RTT's context (settingsContext) holds the
|
|
895
|
+
// ones that mean something, and this one changes every connection.
|
|
896
|
+
var greaseId = 0x1f * Math.floor(Math.random() * 0x10000) + 0x21;
|
|
897
|
+
var settings = concatUint8Arrays([
|
|
898
|
+
buildSettingsPayload(localSettings(context)),
|
|
899
|
+
writeVarInt(greaseId), writeVarInt(Math.floor(Math.random() * 64))
|
|
900
|
+
]);
|
|
891
901
|
|
|
892
902
|
var controlFrame = buildH3Frames([{ frame_type: 0x04, payload: settings }]);
|
|
893
903
|
|
|
@@ -918,6 +928,11 @@ function H3Connection(options) {
|
|
|
918
928
|
}
|
|
919
929
|
|
|
920
930
|
|
|
931
|
+
// Our control and QPACK streams (sendControlStreams).
|
|
932
|
+
function isOwnCriticalStream(sid) {
|
|
933
|
+
return sid === (context.isServer ? 3 : 2) || sid === encoderStreamId || sid === decoderStreamId;
|
|
934
|
+
}
|
|
935
|
+
|
|
921
936
|
// ---- Graceful shutdown: GOAWAY (RFC 9114 §5.2) ----
|
|
922
937
|
|
|
923
938
|
function sendGoaway(id) {
|
|
@@ -1195,6 +1210,15 @@ function H3Connection(options) {
|
|
|
1195
1210
|
// a WT stream, so it can be routed to its session.
|
|
1196
1211
|
listenQuic('stream_reset', function (streamId, code) {
|
|
1197
1212
|
var sid = Number(streamId);
|
|
1213
|
+
// The peer's control or QPACK stream, reset: closed, which RFC 9114
|
|
1214
|
+
// §6.2.1 and RFC 9204 §4.2 make a connection error however it happens.
|
|
1215
|
+
// Only a FIN was caught (processStream); a RESET_STREAM left the
|
|
1216
|
+
// connection serving on without them.
|
|
1217
|
+
var crit = context.streams[sid];
|
|
1218
|
+
if (crit && (crit.type === 'control' || crit.type === 'qpack_enc' || crit.type === 'qpack_dec')) {
|
|
1219
|
+
connectionError(H3_ERR.H3_CLOSED_CRITICAL_STREAM, 'the peer reset its ' + crit.type + ' stream');
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1198
1222
|
var wt = _wtStreams[sid];
|
|
1199
1223
|
var wasParsing = sid in context.streams;
|
|
1200
1224
|
// A client's request stream reset before its HEADERS came: there is no
|
|
@@ -1219,6 +1243,13 @@ function H3Connection(options) {
|
|
|
1219
1243
|
// whole cancellation.
|
|
1220
1244
|
listenQuic('stop_sending', function (streamId, code) {
|
|
1221
1245
|
var sid = Number(streamId);
|
|
1246
|
+
// Asked to stop one of OUR critical streams: QUIC has answered with
|
|
1247
|
+
// RESET_STREAM, so it is closed — the same connection error (§6.2.1:
|
|
1248
|
+
// "A receiver MUST NOT request that the sender close the control stream").
|
|
1249
|
+
if (context.control_streams_sent && isOwnCriticalStream(sid)) {
|
|
1250
|
+
connectionError(H3_ERR.H3_CLOSED_CRITICAL_STREAM, 'the peer stopped our ' + (sid === encoderStreamId ? 'QPACK encoder' : sid === decoderStreamId ? 'QPACK decoder' : 'control') + ' stream');
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1222
1253
|
if (_abortedHere.has(sid)) return;
|
|
1223
1254
|
ev.emit('stop_sending', sid, code);
|
|
1224
1255
|
});
|
|
@@ -1700,7 +1731,7 @@ function H3Connection(options) {
|
|
|
1700
1731
|
// Only the unparsed bytes are copied (once per completed instruction,
|
|
1701
1732
|
// given `need`), and no literal larger than our table can hold is
|
|
1702
1733
|
// waited for.
|
|
1703
|
-
ext = parseQpackEncoderInstructions(
|
|
1734
|
+
ext = parseQpackEncoderInstructions(copyBytes(s, s.buffered),
|
|
1704
1735
|
context.local_qpack_max_table_capacity);
|
|
1705
1736
|
} catch (err) {
|
|
1706
1737
|
// RFC 9204 §4.2: a malformed encoder stream is QPACK_ENCODER_STREAM_ERROR,
|
|
@@ -1708,7 +1739,7 @@ function H3Connection(options) {
|
|
|
1708
1739
|
err.h3ErrorCode = H3_ERR.QPACK_ENCODER_STREAM_ERROR;
|
|
1709
1740
|
throw err;
|
|
1710
1741
|
}
|
|
1711
|
-
consumeBytes(s, ext.
|
|
1742
|
+
consumeBytes(s, ext.consumed);
|
|
1712
1743
|
s.need = ext.need;
|
|
1713
1744
|
if (ext.instructions.length === 0) return;
|
|
1714
1745
|
|
|
@@ -2000,6 +2031,11 @@ function H3Connection(options) {
|
|
|
2000
2031
|
if (avail < headLen + len.value) break;
|
|
2001
2032
|
// Waits for the encoder stream when it must (see sectionBlocked): the
|
|
2002
2033
|
// frame stays in the buffer, read again once unblocked.
|
|
2034
|
+
// Already waiting, and the inserts it needs are not here yet: nothing
|
|
2035
|
+
// to do. The frame was copied and its prefix decoded again on every
|
|
2036
|
+
// packet that arrived on the stream meanwhile.
|
|
2037
|
+
if (_blocked.has(Number(streamId)) &&
|
|
2038
|
+
_blocked.get(Number(streamId)) > context.remote_qpack_table_base_index) return;
|
|
2003
2039
|
var whole = copyBytes(s, headLen + len.value);
|
|
2004
2040
|
var payload = whole.subarray(headLen);
|
|
2005
2041
|
if (sectionBlocked(Number(streamId), payload)) return;
|
|
@@ -2138,7 +2174,11 @@ function H3Connection(options) {
|
|
|
2138
2174
|
var list = [];
|
|
2139
2175
|
for (var hname in headers) {
|
|
2140
2176
|
if (Object.prototype.hasOwnProperty.call(headers, hname)) {
|
|
2141
|
-
list
|
|
2177
|
+
// A list: one field line per value (Set-Cookie above all, which may
|
|
2178
|
+
// not be combined), not String(list).
|
|
2179
|
+
var hv = headers[hname];
|
|
2180
|
+
if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) list.push([hname, String(hv[hi])]); }
|
|
2181
|
+
else list.push([hname, hv]);
|
|
2142
2182
|
}
|
|
2143
2183
|
}
|
|
2144
2184
|
var enc = qpackEncodeHeaderBlock(list, encoderTable, {
|
|
@@ -2155,6 +2195,11 @@ function H3Connection(options) {
|
|
|
2155
2195
|
// Insertions FIRST, on their own stream: they are for the next request,
|
|
2156
2196
|
// not this one.
|
|
2157
2197
|
flushQpackInserts(enc.inserts);
|
|
2198
|
+
if (enc.requiredInsertCount > 0) {
|
|
2199
|
+
var pend = qpackUnacked.get(streamId);
|
|
2200
|
+
if (pend) pend.push(enc.requiredInsertCount);
|
|
2201
|
+
else qpackUnacked.set(streamId, [enc.requiredInsertCount]);
|
|
2202
|
+
}
|
|
2158
2203
|
|
|
2159
2204
|
var frame = buildH3Frames([{ frame_type: 1, payload: new Uint8Array(enc.block) }]);
|
|
2160
2205
|
if (DEBUG) console.log('[h3] sendHeaders stream=' + streamId + ' len=' + frame.byteLength + ' fin=' + !!fin);
|
package/src/h3_client.js
CHANGED
|
@@ -248,6 +248,7 @@ function request(options, callback) {
|
|
|
248
248
|
udpSocket = createQuicClientSocket({
|
|
249
249
|
remoteIp: remoteIp,
|
|
250
250
|
remotePort: remotePort,
|
|
251
|
+
socketBufferSize: options.socketBufferSize,
|
|
251
252
|
// SNI and the name the certificate is checked for: tls.connect's
|
|
252
253
|
// servername where given.
|
|
253
254
|
hostname: options.servername || hostname,
|
|
@@ -476,7 +477,11 @@ function request(options, callback) {
|
|
|
476
477
|
return e;
|
|
477
478
|
});
|
|
478
479
|
});
|
|
479
|
-
|
|
480
|
+
// On 'closing' — the moment the connection is over (a CONNECTION_CLOSE
|
|
481
|
+
// either way, the idle timeout) — not only on 'close', which waits out
|
|
482
|
+
// the draining period (RFC 9000 §10.2, ~3 s): a response the server cut
|
|
483
|
+
// off hung that long before it failed.
|
|
484
|
+
function connectionOver() {
|
|
480
485
|
failAll(0, function (sid) {
|
|
481
486
|
var e = new Error('HTTP/3 connection closed before the response ended');
|
|
482
487
|
e.code = 'ERR_HTTP3_CONNECTION_CLOSED';
|
|
@@ -484,7 +489,9 @@ function request(options, callback) {
|
|
|
484
489
|
e.retryable = sid >= conn._goawayId;
|
|
485
490
|
return e;
|
|
486
491
|
});
|
|
487
|
-
}
|
|
492
|
+
}
|
|
493
|
+
conn.quic.on('closing', connectionOver);
|
|
494
|
+
conn.quic.on('close', connectionOver);
|
|
488
495
|
}
|
|
489
496
|
|
|
490
497
|
|
|
@@ -662,17 +669,28 @@ function request(options, callback) {
|
|
|
662
669
|
else if (reqBody.length) startStreaming();
|
|
663
670
|
}
|
|
664
671
|
|
|
665
|
-
|
|
672
|
+
// contentLength: the body's size when it is known whole (ended before it
|
|
673
|
+
// went) — sent as h2_client and node:https send it, and as browsers do. A
|
|
674
|
+
// server that decides whether there is a body from the headers (Express's
|
|
675
|
+
// body-parser: content-length or transfer-encoding) saw none over HTTP/3,
|
|
676
|
+
// and req.body came out empty.
|
|
677
|
+
function requestHeaders(contentLength) {
|
|
666
678
|
var h3Headers = {
|
|
667
679
|
':method': method,
|
|
668
680
|
':scheme': 'https',
|
|
669
681
|
':authority': hostname + (port !== 443 ? ':' + port : ''),
|
|
670
682
|
':path': path
|
|
671
683
|
};
|
|
672
|
-
|
|
684
|
+
// A repeated field (an array) stays one line per value: String(list)
|
|
685
|
+
// made two cookies "a=1,b=2", which the server reads as one cookie.
|
|
686
|
+
for (var name in headers) {
|
|
687
|
+
var v = headers[name];
|
|
688
|
+
h3Headers[name.toLowerCase()] = Array.isArray(v) ? v.map(String) : String(v);
|
|
689
|
+
}
|
|
673
690
|
if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
|
|
674
691
|
if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
|
|
675
692
|
if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
|
|
693
|
+
if (contentLength != null && h3Headers['content-length'] === undefined) h3Headers['content-length'] = String(contentLength);
|
|
676
694
|
return h3Headers;
|
|
677
695
|
}
|
|
678
696
|
|
|
@@ -687,7 +705,9 @@ function request(options, callback) {
|
|
|
687
705
|
// Early: kept, to go again if the server refuses it.
|
|
688
706
|
if (earlyTried && !handshaken) earlyBody = { body: body.slice(), trailers: reqTrailers };
|
|
689
707
|
if (body.length === 0 && !reqTrailers) { h3.sendHeaders(streamId, requestHeaders(), true); return; }
|
|
690
|
-
|
|
708
|
+
var bodyLength = 0;
|
|
709
|
+
for (var bi = 0; bi < body.length; bi++) bodyLength += body[bi].byteLength;
|
|
710
|
+
h3.sendHeaders(streamId, requestHeaders(body.length ? bodyLength : null), false);
|
|
691
711
|
for (var i = 0; i < body.length; i++) h3.sendBody(streamId, body[i], i === body.length - 1 && !reqTrailers);
|
|
692
712
|
if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
|
|
693
713
|
}
|
package/src/h3_server.js
CHANGED
|
@@ -28,7 +28,8 @@ import { createQuicServer } from './quic_server.js';
|
|
|
28
28
|
import { pickTlsOptions } from './tls_options.js';
|
|
29
29
|
import { _makeSocketStub } from './streams.js';
|
|
30
30
|
import { H3Connection, H3_ERR, wtCloseCapsule, wtDrainCapsule, createCapsuleReader, h3SettingsContext } from './h3.js';
|
|
31
|
-
import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats
|
|
31
|
+
import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats,
|
|
32
|
+
exporterArgs, exportSessionKeyingMaterial } from './wt_stream.js';
|
|
32
33
|
import { contextZero, datagramCapsule, STREAM_QUEUE_MAX } from './connect_udp.js';
|
|
33
34
|
|
|
34
35
|
|
|
@@ -132,6 +133,7 @@ function createServer(options, handler) {
|
|
|
132
133
|
ech: options.ech || null,
|
|
133
134
|
ocspRequest: options.ocspRequest || null,
|
|
134
135
|
resetTokenSecret: options.resetTokenSecret || null,
|
|
136
|
+
tokenSecret: options.tokenSecret || null,
|
|
135
137
|
validateAddress: options.validateAddress === true,
|
|
136
138
|
handshakeLoadThreshold: options.handshakeLoadThreshold,
|
|
137
139
|
earlyData: options.earlyData, earlyDataMaxAge: options.earlyDataMaxAge, earlyDataReplay: options.earlyDataReplay,
|
|
@@ -151,6 +153,7 @@ function createServer(options, handler) {
|
|
|
151
153
|
maxUdpPayloadSize: options.maxUdpPayloadSize,
|
|
152
154
|
initialRtt: options.initialRtt,
|
|
153
155
|
reorderWindow: options.reorderWindow,
|
|
156
|
+
socketBufferSize: options.socketBufferSize,
|
|
154
157
|
connectionOptions: options.connectionOptions,
|
|
155
158
|
quicVersions: options.quicVersions,
|
|
156
159
|
socket: options.socket || null,
|
|
@@ -607,10 +610,15 @@ function createServer(options, handler) {
|
|
|
607
610
|
// writeHead, when the status is known.
|
|
608
611
|
_bodyAllowed: true,
|
|
609
612
|
|
|
613
|
+
// A list is kept a list, and goes as one field line per value, as
|
|
614
|
+
// HTTP/1.1 and HTTP/2 send it here and as Node's http does. It was
|
|
615
|
+
// String(list): two cookies became one "a=1,b=2", which a browser
|
|
616
|
+
// reads as a single cookie a with the value "1,b=2" (Set-Cookie
|
|
617
|
+
// never combines, RFC 9110 §5.3).
|
|
610
618
|
setHeader: function (name, value) {
|
|
611
619
|
validateHeaderName(name);
|
|
612
620
|
validateHeaderValue(name, value);
|
|
613
|
-
res.headers[name.toLowerCase()] = String(value);
|
|
621
|
+
res.headers[name.toLowerCase()] = Array.isArray(value) ? value.map(String) : String(value);
|
|
614
622
|
},
|
|
615
623
|
|
|
616
624
|
getHeader: function (name) {
|
|
@@ -640,7 +648,7 @@ function createServer(options, handler) {
|
|
|
640
648
|
}
|
|
641
649
|
// Date (RFC 9110 §6.6.1), as HTTP/2 and HTTP/1.1 send it — HTTP/3's
|
|
642
650
|
// responses had none.
|
|
643
|
-
if (!('date' in h3Headers) && statusCode >= 200) h3Headers['date'] = httpDate();
|
|
651
|
+
if (!('date' in h3Headers) && statusCode >= 200 && !(res._wrapper && res._wrapper.sendDate === false)) h3Headers['date'] = httpDate();
|
|
644
652
|
|
|
645
653
|
if (conn.h3) {
|
|
646
654
|
// Send headers WITHOUT FIN for WT (session stays open)
|
|
@@ -703,6 +711,12 @@ function createServer(options, handler) {
|
|
|
703
711
|
if (typeof callback === 'function') process.nextTick(callback);
|
|
704
712
|
},
|
|
705
713
|
|
|
714
|
+
// 102 Processing (node:http's writeProcessing): as Early Hints go.
|
|
715
|
+
writeProcessing: function () {
|
|
716
|
+
if (res.headersSent || res._finished || !conn.h3) return;
|
|
717
|
+
try { conn.h3.sendHeaders(streamId, { ':status': '102' }, false); } catch (e) { /* stream gone */ }
|
|
718
|
+
},
|
|
719
|
+
|
|
706
720
|
write: function (chunk) {
|
|
707
721
|
if (!res.headersSent) res.writeHead(res.statusCode);
|
|
708
722
|
if (!res._bodyAllowed) return true;
|
|
@@ -868,6 +882,17 @@ function createServer(options, handler) {
|
|
|
868
882
|
// datagram's.
|
|
869
883
|
createSendGroup: function () { return createSendGroup(); },
|
|
870
884
|
|
|
885
|
+
// The browser's exportKeyingMaterial, as the client's: this session's
|
|
886
|
+
// keying material from the TLS exporter — the same bytes the client
|
|
887
|
+
// gets for the same label and context.
|
|
888
|
+
exportKeyingMaterial: function (label, context, outputLength) {
|
|
889
|
+
var a;
|
|
890
|
+
try { a = exporterArgs(arguments); } catch (e) { return Promise.reject(e); }
|
|
891
|
+
if (!req._isWebTransport || req._wtClosed) return Promise.reject(new DOMException('the session is closed', 'InvalidStateError'));
|
|
892
|
+
try { return Promise.resolve(exportSessionKeyingMaterial(conn.quic, Number(streamId), a)); }
|
|
893
|
+
catch (e) { return Promise.reject(e); }
|
|
894
|
+
},
|
|
895
|
+
|
|
871
896
|
// A stream of ours: a Duplex, or a Writable — recorded with its
|
|
872
897
|
// session, so the session's end destroys it. options { sendOrder,
|
|
873
898
|
// sendGroup, waitUntilAvailable }, the browser's (wt_stream's
|
|
@@ -929,6 +954,13 @@ function createServer(options, handler) {
|
|
|
929
954
|
for (var name in params.request_headers) {
|
|
930
955
|
stream.req.headers[name] = params.request_headers[name];
|
|
931
956
|
}
|
|
957
|
+
// The field lines as they came (validateFieldSection), which the
|
|
958
|
+
// copy above leaves behind — not enumerable: req.rawHeaders and
|
|
959
|
+
// headersDistinct are built from them. Without it they were rebuilt
|
|
960
|
+
// from the joined values, two x-a lines one "1, 2".
|
|
961
|
+
if (params.request_headers._rawHeaders && !stream.req.headers._rawHeaders) {
|
|
962
|
+
Object.defineProperty(stream.req.headers, '_rawHeaders', { value: params.request_headers._rawHeaders, enumerable: false });
|
|
963
|
+
}
|
|
932
964
|
|
|
933
965
|
// Extract :method and :path
|
|
934
966
|
if (stream.req.method === null && ':method' in stream.req.headers) {
|
|
@@ -1023,6 +1055,7 @@ function createServer(options, handler) {
|
|
|
1023
1055
|
releaseHttpStream(conn, streamId, false);
|
|
1024
1056
|
return;
|
|
1025
1057
|
}
|
|
1058
|
+
stream.req.upgrade = true;
|
|
1026
1059
|
ev.emit('upgrade', stream.req, duplex, Buffer.alloc(0));
|
|
1027
1060
|
return;
|
|
1028
1061
|
}
|
|
@@ -1188,6 +1221,10 @@ function createServer(options, handler) {
|
|
|
1188
1221
|
return sock;
|
|
1189
1222
|
}
|
|
1190
1223
|
|
|
1224
|
+
// Every connection, for closing them (closeIdleConnections, close()).
|
|
1225
|
+
var conns = new Set();
|
|
1226
|
+
var closingServer = null; // close()'s callback, once every connection is gone
|
|
1227
|
+
|
|
1191
1228
|
qserver.on('connection', function (quic, peer) {
|
|
1192
1229
|
var conn = { quic: quic, h3: null, http_streams: {}, _wtStreamObjects: {}, served: 0,
|
|
1193
1230
|
headerDeadlines: {}, // request streams whose HEADERS is still arriving
|
|
@@ -1208,7 +1245,12 @@ function createServer(options, handler) {
|
|
|
1208
1245
|
// 'aborted' on the request and 'close' on the response, a tunnel
|
|
1209
1246
|
// destroyed, a session closed with its streams. Requests that had
|
|
1210
1247
|
// finished in both directions are already gone (releaseIfDone).
|
|
1248
|
+
conns.add(conn);
|
|
1249
|
+
// Over for the application ('closing'), or its draining done ('close').
|
|
1250
|
+
var gone = function () { if (conns.delete(conn)) connGone(); };
|
|
1251
|
+
quic.on('closing', gone);
|
|
1211
1252
|
quic.on('close', function () {
|
|
1253
|
+
gone();
|
|
1212
1254
|
for (var hid in conn.headerDeadlines) clearHeadersDeadline(conn, hid);
|
|
1213
1255
|
for (var sid in conn.http_streams) releaseHttpStream(conn, sid, true);
|
|
1214
1256
|
for (var wid in conn._wtStreamObjects) releaseHttpStream(conn, wid, true);
|
|
@@ -1615,9 +1657,63 @@ function createServer(options, handler) {
|
|
|
1615
1657
|
// server. This layer adds HTTP/3 request events on top.
|
|
1616
1658
|
// ============================================================
|
|
1617
1659
|
|
|
1660
|
+
// ---- Closing, as node:http (see h1_server) ----
|
|
1661
|
+
//
|
|
1662
|
+
// Idle: no request open on it — before its handshake too, as node:http
|
|
1663
|
+
// counts a connection that has not sent a request. GOAWAY, and closed at
|
|
1664
|
+
// once (closeAfterGoaway: nothing to wait for); before the handshake,
|
|
1665
|
+
// refused (goaway() does that).
|
|
1666
|
+
function isIdle(conn) { return Object.keys(conn.http_streams).length === 0; }
|
|
1667
|
+
// GOAWAY and CONNECTION_CLOSE together (§5.2 allows closing at once when
|
|
1668
|
+
// nothing is in flight): waiting for the GOAWAY's acknowledgement, a
|
|
1669
|
+
// client that had gone held the close until its idle timeout.
|
|
1670
|
+
function closeIdle(conn) {
|
|
1671
|
+
if (!conn.handle) return;
|
|
1672
|
+
conn.handle.goaway();
|
|
1673
|
+
conn.handle.destroy();
|
|
1674
|
+
}
|
|
1675
|
+
function closeIdleConnections() {
|
|
1676
|
+
for (var conn of conns) if (isIdle(conn)) closeIdle(conn);
|
|
1677
|
+
}
|
|
1678
|
+
function closeAllConnections() {
|
|
1679
|
+
for (var conn of conns) if (conn.handle) conn.handle.destroy();
|
|
1680
|
+
}
|
|
1681
|
+
// No new connections; GOAWAY on each — the requests in flight finish, then
|
|
1682
|
+
// the connection closes (closeAfterGoaway) — and once none is left, the
|
|
1683
|
+
// UDP sockets. It closed every connection on the spot: a response being
|
|
1684
|
+
// sent was cut off, and its client waited for the rest until a timeout.
|
|
1685
|
+
function close(callback) {
|
|
1686
|
+
qserver.stopAccepting();
|
|
1687
|
+
closingServer = typeof callback === 'function' ? callback : function () {};
|
|
1688
|
+
for (var conn of conns) {
|
|
1689
|
+
if (isIdle(conn)) closeIdle(conn);
|
|
1690
|
+
else if (conn.handle) conn.handle.goaway();
|
|
1691
|
+
}
|
|
1692
|
+
connGone();
|
|
1693
|
+
}
|
|
1694
|
+
function connGone() {
|
|
1695
|
+
if (!closingServer || conns.size > 0) return;
|
|
1696
|
+
var cb = closingServer;
|
|
1697
|
+
closingServer = null;
|
|
1698
|
+
qserver.close(cb);
|
|
1699
|
+
}
|
|
1700
|
+
function setLimits(o) {
|
|
1701
|
+
if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
|
|
1702
|
+
if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
|
|
1703
|
+
if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1618
1706
|
return {
|
|
1619
|
-
listen: qserver.listen,
|
|
1620
|
-
|
|
1707
|
+
listen: function (p, h, cb) { closingServer = null; return qserver.listen(p, h, cb); },
|
|
1708
|
+
// The UDP socket's address — after listen(0), the port the OS gave.
|
|
1709
|
+
address: function () { return qserver.address(); },
|
|
1710
|
+
close: close,
|
|
1711
|
+
closeIdleConnections: closeIdleConnections,
|
|
1712
|
+
closeAllConnections: closeAllConnections,
|
|
1713
|
+
setLimits: setLimits,
|
|
1714
|
+
get connectionCount() { return conns.size; },
|
|
1715
|
+
ref: qserver.ref,
|
|
1716
|
+
unref: qserver.unref,
|
|
1621
1717
|
on: function (name, fn) { if (name === 'upgrade') upgradeListeners++; ev.on(name, fn); },
|
|
1622
1718
|
setTimeout: function () { /* TODO */ },
|
|
1623
1719
|
|
|
@@ -1639,7 +1735,9 @@ function createServer(options, handler) {
|
|
|
1639
1735
|
getOptions: qserver.getOptions,
|
|
1640
1736
|
setOptions: qserver.setOptions,
|
|
1641
1737
|
getResetTokenSecret: qserver.getResetTokenSecret,
|
|
1642
|
-
setResetTokenSecret: qserver.setResetTokenSecret
|
|
1738
|
+
setResetTokenSecret: qserver.setResetTokenSecret,
|
|
1739
|
+
getTokenSecret: qserver.getTokenSecret,
|
|
1740
|
+
setTokenSecret: qserver.setTokenSecret
|
|
1643
1741
|
};
|
|
1644
1742
|
}
|
|
1645
1743
|
|