quico 0.4.0 → 0.8.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 +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/h3_client.js
CHANGED
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
|
|
15
15
|
import dns from 'node:dns';
|
|
16
16
|
import { DEBUG } from './utils.js';
|
|
17
|
-
import { H3Connection } from './h3.js';
|
|
17
|
+
import { H3Connection, H3_ERR_NAME } from './h3.js';
|
|
18
18
|
import { createQuicClientSocket } from './quic_socket.js';
|
|
19
|
+
import { parseVersionList } from './quic_version.js';
|
|
20
|
+
import { checkConnectionOptions, CONNECTION_OPTIONS } from './quic_connection.js';
|
|
21
|
+
import { trailerFields, formatPriority } from './http_common.js';
|
|
22
|
+
|
|
23
|
+
// What the body takes before write() says wait — written while connecting,
|
|
24
|
+
// or not yet sent on the stream.
|
|
25
|
+
var BODY_HIGH_WATER = 64 * 1024;
|
|
19
26
|
|
|
20
27
|
|
|
21
28
|
// ============================================================
|
|
@@ -40,6 +47,11 @@ function request(options, callback) {
|
|
|
40
47
|
// one transport this library exists for the only one that can't reach a
|
|
41
48
|
// self-signed or private-CA server (H2/H1.1 fallbacks honour the option).
|
|
42
49
|
var rejectUnauthorized = options.rejectUnauthorized !== false;
|
|
50
|
+
// Checked now, as https.request checks its options: thrown here, not from
|
|
51
|
+
// inside the socket's callback later.
|
|
52
|
+
parseVersionList(options.quicVersions, 'quicVersions');
|
|
53
|
+
// The connection options it sets, checked now (thrown here, as above).
|
|
54
|
+
(function () { var o = {}; for (var k in CONNECTION_OPTIONS) if (k !== 'carefulResume' && options[k] != null) o[k] = options[k]; checkConnectionOptions(o, 'request'); })();
|
|
43
55
|
var ca = options.ca || null;
|
|
44
56
|
var port = options.port || 443;
|
|
45
57
|
var path = options.path || '/';
|
|
@@ -65,6 +77,28 @@ function request(options, callback) {
|
|
|
65
77
|
var udpSocket = existingConn ? existingConn.udpSocket : null;
|
|
66
78
|
var streamId = null; // assigned when connection is ready
|
|
67
79
|
var connected = existingConn ? true : false;
|
|
80
|
+
var requestSent = false; // HEADERS went out on streamId
|
|
81
|
+
var readyDone = false; // the stream exists and the request is this one's to send (ready)
|
|
82
|
+
// 0-RTT (RFC 9001 §4.6, RFC 8470): with earlyData and a saved session, a
|
|
83
|
+
// request that may be replayed safely — GET, HEAD, OPTIONS — goes before
|
|
84
|
+
// the handshake ends. res.earlyData: 'accepted' (it went early and was
|
|
85
|
+
// taken), 'rejected' (refused — by the server's TLS, or by a 425 — and
|
|
86
|
+
// sent again after the handshake), null (not tried).
|
|
87
|
+
// Decided when a connection is made (startConnection): the session is
|
|
88
|
+
// read there — the unified client's is taken from its agent on that read,
|
|
89
|
+
// and a request on a connection already open must not use one up.
|
|
90
|
+
var earlyWanted = false;
|
|
91
|
+
var earlyTried = false; // set up on the early key (onEarly)
|
|
92
|
+
var earlyResult = null;
|
|
93
|
+
var earlyBody = null; // what went early, to send again if refused
|
|
94
|
+
var handshaken = false; // onConnect has come
|
|
95
|
+
var retry425 = false; // the server answered the early request 425: again, after the handshake
|
|
96
|
+
var after425 = null; // to run at onConnect, when the 425 came first
|
|
97
|
+
var streaming = false; // HEADERS sent before the body ended: DATA follows as written
|
|
98
|
+
var reqBodyBytes = 0, needDrain = false, drainWaiting = false;
|
|
99
|
+
var reqTrailers = null;
|
|
100
|
+
var finished = false; // the response ended or failed
|
|
101
|
+
var cancelled = false; // destroy() was called
|
|
68
102
|
|
|
69
103
|
var clientReq = {
|
|
70
104
|
on: function (event, fn) {
|
|
@@ -72,22 +106,70 @@ function request(options, callback) {
|
|
|
72
106
|
return clientReq;
|
|
73
107
|
},
|
|
74
108
|
|
|
109
|
+
// The body, as node:http's: held while connecting (write() false past
|
|
110
|
+
// BODY_HIGH_WATER, then 'drain'), then streamed — HEADERS on the first
|
|
111
|
+
// write once the stream exists, DATA as written, with the stream's
|
|
112
|
+
// backpressure.
|
|
75
113
|
write: function (chunk) {
|
|
114
|
+
if (finished || cancelled || reqFinished) return false;
|
|
76
115
|
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
116
|
+
if (!chunk || chunk.byteLength === 0) return !needDrain;
|
|
117
|
+
if (!streaming && readyDone && !requestSent) startStreaming();
|
|
118
|
+
if (streaming) return streamOut(chunk);
|
|
77
119
|
reqBody.push(chunk);
|
|
120
|
+
reqBodyBytes += chunk.byteLength;
|
|
121
|
+
if (reqBodyBytes >= BODY_HIGH_WATER) { needDrain = true; return false; }
|
|
78
122
|
return true;
|
|
79
123
|
},
|
|
80
124
|
|
|
81
|
-
|
|
82
|
-
|
|
125
|
+
// RFC 9218: the `priority` header while the request has not gone, a
|
|
126
|
+
// PRIORITY_UPDATE on the control stream after.
|
|
127
|
+
setPriority: function (p) {
|
|
128
|
+
if (requestSent) { if (h3 && !finished && !cancelled) h3.sendPriorityUpdate(streamId, p); return; }
|
|
129
|
+
for (var k in headers) if (k.toLowerCase() === 'priority') delete headers[k];
|
|
130
|
+
var v = formatPriority(p);
|
|
131
|
+
if (v) headers.priority = v;
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// `trailers` (node:http's addTrailers, through the unified client): a
|
|
135
|
+
// HEADERS frame after the body, ending the stream (RFC 9114 §4.1).
|
|
136
|
+
end: function (chunk, trailers) {
|
|
137
|
+
if (reqFinished) return clientReq;
|
|
138
|
+
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
139
|
+
reqTrailers = trailerFields(trailers);
|
|
140
|
+
if (streaming) {
|
|
141
|
+
reqFinished = true;
|
|
142
|
+
if (chunk && chunk.byteLength) h3.sendBody(streamId, chunk, !reqTrailers);
|
|
143
|
+
else if (!reqTrailers) h3.sendBody(streamId, null, true);
|
|
144
|
+
if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
|
|
145
|
+
return clientReq;
|
|
146
|
+
}
|
|
147
|
+
if (chunk && chunk.byteLength) { reqBody.push(chunk); reqBodyBytes += chunk.byteLength; }
|
|
83
148
|
reqFinished = true;
|
|
84
|
-
if (
|
|
149
|
+
if (readyDone) sendRequest();
|
|
85
150
|
return clientReq;
|
|
86
151
|
},
|
|
87
152
|
|
|
153
|
+
// Cancel this request. Its stream is what ends (RFC 9114 §4.1.1:
|
|
154
|
+
// STOP_SENDING and RESET_STREAM with H3_REQUEST_CANCELLED) — not the
|
|
155
|
+
// connection, which may carry other requests (a pooled one does). This
|
|
156
|
+
// closed the whole connection, every other request on it included.
|
|
157
|
+
// A connection that is this request's alone is closed with it.
|
|
88
158
|
destroy: function () {
|
|
89
|
-
if (
|
|
90
|
-
|
|
159
|
+
if (cancelled || finished) return;
|
|
160
|
+
cancelled = true;
|
|
161
|
+
var conn = clientReq._connection || existingConn;
|
|
162
|
+
if (conn && conn._streamHandlers && streamId !== null) delete conn._streamHandlers[streamId];
|
|
163
|
+
// Still connecting: closed in onConnect (or ends by itself if the
|
|
164
|
+
// handshake fails) — closing the socket under a handshake in progress
|
|
165
|
+
// would leave its timers sending into a closed socket.
|
|
166
|
+
if (autoClose) { if (quic) closeConnection(); return; }
|
|
167
|
+
// Sent or not: its stream ID is taken, and a later request's higher
|
|
168
|
+
// one opens it at the server (RFC 9000 §3.2) — where nothing would
|
|
169
|
+
// ever end it, a stream of the connection's limit lost for good. So it
|
|
170
|
+
// is cancelled either way; never written, its RESET_STREAM carries a
|
|
171
|
+
// final size of 0 (sendRequest() will not run).
|
|
172
|
+
if (h3 && streamId !== null) h3.abortStream(streamId, 0x10c); // H3_REQUEST_CANCELLED
|
|
91
173
|
}
|
|
92
174
|
};
|
|
93
175
|
|
|
@@ -95,14 +177,26 @@ function request(options, callback) {
|
|
|
95
177
|
var clientRes = {
|
|
96
178
|
statusCode: 0,
|
|
97
179
|
headers: {},
|
|
180
|
+
trailers: {},
|
|
181
|
+
rawTrailers: [],
|
|
98
182
|
_listeners: {},
|
|
99
183
|
_headersReceived: false,
|
|
100
184
|
|
|
101
185
|
on: function (event, fn) {
|
|
102
186
|
(clientRes._listeners[event] = clientRes._listeners[event] || []).push(fn);
|
|
103
187
|
return clientRes;
|
|
104
|
-
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// The body's flow-control credit, returned as the layer above reads it
|
|
191
|
+
// rather than as it arrives (h3's holdBody) — as the server does with a
|
|
192
|
+
// request body. Credited on delivery, a response nobody read yet was
|
|
193
|
+
// taken in as fast as the server could send it: a slow consumer of a
|
|
194
|
+
// large download held all of it in memory, and the server never slowed.
|
|
195
|
+
_holdBody: function () { if (respConn && respConn.h3) respConn.h3.holdBody(streamId); },
|
|
196
|
+
_creditBody: function (n) { if (respConn && respConn.h3) respConn.h3.creditBody(streamId, n); },
|
|
197
|
+
_releaseBody: function () { if (respConn && respConn.h3) respConn.h3.releaseBody(streamId); }
|
|
105
198
|
};
|
|
199
|
+
var respConn = null; // the connection the response comes on (setupResponseHandlers)
|
|
106
200
|
|
|
107
201
|
function emit(obj, event, data) {
|
|
108
202
|
var listeners = (obj._listeners || obj)[event] || [];
|
|
@@ -122,7 +216,7 @@ function request(options, callback) {
|
|
|
122
216
|
streamId = allocateStreamId(existingConn);
|
|
123
217
|
installSharedHandlers(existingConn);
|
|
124
218
|
setupResponseHandlers(existingConn);
|
|
125
|
-
|
|
219
|
+
ready(existingConn);
|
|
126
220
|
} else {
|
|
127
221
|
resolveAndConnect(hostname, port);
|
|
128
222
|
}
|
|
@@ -147,20 +241,122 @@ function request(options, callback) {
|
|
|
147
241
|
|
|
148
242
|
|
|
149
243
|
function startConnection(remoteIp, remotePort) {
|
|
244
|
+
if (cancelled) return;
|
|
245
|
+
var sessionNow = options.session || null;
|
|
246
|
+
earlyWanted = options.earlyData === true && !!sessionNow &&
|
|
247
|
+
(method === 'GET' || method === 'HEAD' || method === 'OPTIONS');
|
|
150
248
|
udpSocket = createQuicClientSocket({
|
|
151
249
|
remoteIp: remoteIp,
|
|
152
250
|
remotePort: remotePort,
|
|
153
|
-
|
|
251
|
+
// SNI and the name the certificate is checked for: tls.connect's
|
|
252
|
+
// servername where given.
|
|
253
|
+
hostname: options.servername || hostname,
|
|
154
254
|
rejectUnauthorized: rejectUnauthorized,
|
|
155
255
|
ca: ca,
|
|
256
|
+
// A client certificate, for a server that asks (https.request's names).
|
|
257
|
+
key: options.key || null, cert: options.cert || null, passphrase: options.passphrase,
|
|
258
|
+
// A saved session to resume with, and the request's 'session' event for
|
|
259
|
+
// each the server sends — as https.request does (its socket's
|
|
260
|
+
// 'session'). Only the request that opened the connection has one: a
|
|
261
|
+
// request on a connection already open sees no handshake.
|
|
262
|
+
session: sessionNow,
|
|
263
|
+
// The server's stapled OCSP response, as the request's 'OCSPResponse'
|
|
264
|
+
// (tls.connect's requestOCSP) — on the request that opened the connection.
|
|
265
|
+
requestOCSP: options.requestOCSP === true,
|
|
266
|
+
// Encrypted Client Hello: the server's ECHConfigList, or GREASE ECH.
|
|
267
|
+
ech: options.ech || null,
|
|
268
|
+
echGrease: options.echGrease === true,
|
|
269
|
+
// The key-exchange groups to offer (tls.connect's ecdhCurve).
|
|
270
|
+
ecdhCurve: options.ecdhCurve,
|
|
271
|
+
// The signature schemes to offer (tls.connect's sigalgs).
|
|
272
|
+
sigalgs: options.sigalgs,
|
|
273
|
+
quicVersions: options.quicVersions,
|
|
274
|
+
// This client's connection options (quic_connection's CONNECTION_OPTIONS):
|
|
275
|
+
// Startup's cap on what it sends (an upload's), path MTU discovery, the
|
|
276
|
+
// largest UDP payload, the RTT assumed before a sample, the reordering
|
|
277
|
+
// a packet is allowed, the initial window.
|
|
278
|
+
startupCap: options.startupCap,
|
|
279
|
+
pmtuDiscovery: options.pmtuDiscovery,
|
|
280
|
+
maxUdpPayloadSize: options.maxUdpPayloadSize,
|
|
281
|
+
initialRtt: options.initialRtt,
|
|
282
|
+
reorderWindow: options.reorderWindow,
|
|
283
|
+
initialCongestionWindow: options.initialCongestionWindow,
|
|
284
|
+
// A NEW_TOKEN token for the Initial (the agent keeps them), and the
|
|
285
|
+
// request's 'newToken' event for the one the server gives for next time.
|
|
286
|
+
token: options.token || null,
|
|
287
|
+
earlyData: earlyWanted,
|
|
288
|
+
onEarly: function (q, s) {
|
|
289
|
+
if (cancelled) return;
|
|
290
|
+
earlyTried = true;
|
|
291
|
+
earlyResult = 'accepted'; // until the server says otherwise
|
|
292
|
+
setUp(q, s);
|
|
293
|
+
},
|
|
294
|
+
onSocket: function (q) {
|
|
295
|
+
q.on('session', function (buf) { emit(reqListeners, 'session', buf); });
|
|
296
|
+
q.on('new_token', function (buf) { emit(reqListeners, 'newToken', buf); });
|
|
297
|
+
q.on('OCSPResponse', function (buf) { emit(reqListeners, 'OCSPResponse', buf); });
|
|
298
|
+
},
|
|
156
299
|
|
|
157
300
|
onError: function (err) {
|
|
301
|
+
if (cancelled) return;
|
|
158
302
|
emit(reqListeners, 'error', err);
|
|
159
303
|
},
|
|
160
304
|
|
|
161
305
|
onConnect: function (q, s) {
|
|
306
|
+
// Sent early: taken, nothing more to do — or refused, and the
|
|
307
|
+
// connection has forgotten it: HTTP/3 starts over on it, and the
|
|
308
|
+
// request goes again, in 1-RTT.
|
|
309
|
+
if (earlyTried) {
|
|
310
|
+
handshaken = true;
|
|
311
|
+
if (cancelled) { closeConnection(); return; }
|
|
312
|
+
if (after425) { var a425 = after425; after425 = null; a425(); return; }
|
|
313
|
+
if (q.earlyDataStatus !== 'rejected') return;
|
|
314
|
+
earlyResult = 'rejected';
|
|
315
|
+
redoAfterEarly();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
setUp(q, s);
|
|
319
|
+
}
|
|
320
|
+
// onClose: the helper closes the UDP socket; nothing extra needed here.
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// What was sent early, refused: this request's stream and the HTTP/3
|
|
325
|
+
// layer it went through are dropped, and both made again.
|
|
326
|
+
function redoAfterEarly() {
|
|
327
|
+
var old = respConn;
|
|
328
|
+
if (old && old._streamHandlers) delete old._streamHandlers[streamId];
|
|
329
|
+
if (h3 && h3.detach) h3.detach();
|
|
330
|
+
requestSent = false; readyDone = false; streaming = false;
|
|
331
|
+
if (earlyBody) { reqBody = earlyBody.body; reqTrailers = earlyBody.trailers; reqBodyBytes = 0; }
|
|
332
|
+
setUp(quic, udpSocket);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// The early request answered 425: on a new stream of the same connection,
|
|
336
|
+
// once the handshake is done (now, or at onConnect).
|
|
337
|
+
function againAfter425() {
|
|
338
|
+
function go() {
|
|
339
|
+
if (cancelled || finished) return;
|
|
340
|
+
var conn = respConn;
|
|
341
|
+
requestSent = false; readyDone = false; streaming = false;
|
|
342
|
+
if (earlyBody) { reqBody = earlyBody.body; reqTrailers = earlyBody.trailers; }
|
|
343
|
+
earlyBody = null;
|
|
344
|
+
retry425 = false;
|
|
345
|
+
streamId = allocateStreamId(conn);
|
|
346
|
+
setupResponseHandlers(conn);
|
|
347
|
+
ready(conn);
|
|
348
|
+
}
|
|
349
|
+
if (handshaken) go(); else after425 = go;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function setUp(q, s) {
|
|
353
|
+
(function () {
|
|
162
354
|
quic = q;
|
|
163
355
|
udpSocket = s;
|
|
356
|
+
// Cancelled while connecting: the connection was for this request
|
|
357
|
+
// (an owned one, or a pool lead whose followers have been released),
|
|
358
|
+
// and nobody else holds it.
|
|
359
|
+
if (cancelled) { closeConnection(); return; }
|
|
164
360
|
if (DEBUG) console.log('[client] QUIC connected');
|
|
165
361
|
|
|
166
362
|
h3 = new H3Connection({ quicConnection: quic, isServer: false });
|
|
@@ -178,12 +374,8 @@ function request(options, callback) {
|
|
|
178
374
|
clientReq._connection = conn;
|
|
179
375
|
|
|
180
376
|
setupResponseHandlers(conn);
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
if (reqFinished) sendRequest();
|
|
184
|
-
}
|
|
185
|
-
// onClose: the helper closes the UDP socket; nothing extra needed here.
|
|
186
|
-
});
|
|
377
|
+
ready(conn);
|
|
378
|
+
})();
|
|
187
379
|
}
|
|
188
380
|
|
|
189
381
|
|
|
@@ -219,12 +411,86 @@ function request(options, callback) {
|
|
|
219
411
|
var h = conn._streamHandlers[sid];
|
|
220
412
|
if (h) h.onBody(data);
|
|
221
413
|
});
|
|
414
|
+
h3c.on('http_trailers', function (sid, trailers) {
|
|
415
|
+
var h = conn._streamHandlers[sid];
|
|
416
|
+
if (h && h.onTrailers) h.onTrailers(trailers);
|
|
417
|
+
});
|
|
222
418
|
h3c.on('http_end', function (sid) {
|
|
223
419
|
var h = conn._streamHandlers[sid];
|
|
224
420
|
if (!h) return;
|
|
225
421
|
delete conn._streamHandlers[sid]; // free per-stream state before dispatch
|
|
226
422
|
h.onEnd();
|
|
227
423
|
});
|
|
424
|
+
// The response stream was ended over what the server sent on it (an
|
|
425
|
+
// oversized HEADERS, a stream with no response): the request fails,
|
|
426
|
+
// rather than waiting for an end that will not come.
|
|
427
|
+
h3c.on('stream_error', function (sid, code, reason) {
|
|
428
|
+
var h = conn._streamHandlers[sid];
|
|
429
|
+
if (!h) return;
|
|
430
|
+
delete conn._streamHandlers[sid];
|
|
431
|
+
if (h.onError) h.onError(code, reason);
|
|
432
|
+
});
|
|
433
|
+
// The server reset the request stream (RESET_STREAM): the response, or
|
|
434
|
+
// what is left of it, will not come. Nothing listened, and a response
|
|
435
|
+
// cut off this way never ended nor failed — it hung.
|
|
436
|
+
h3c.on('stream_reset', function (sid, code) {
|
|
437
|
+
var h = conn._streamHandlers[sid];
|
|
438
|
+
if (!h) return;
|
|
439
|
+
delete conn._streamHandlers[sid];
|
|
440
|
+
if (h.onReset) h.onReset(code);
|
|
441
|
+
});
|
|
442
|
+
// The server will not read the rest of the request (STOP_SENDING; QUIC
|
|
443
|
+
// has reset our sending part in answer). Not heard before: a request
|
|
444
|
+
// the server refused this way went on "writing" into nothing, and was
|
|
445
|
+
// never told.
|
|
446
|
+
h3c.on('stop_sending', function (sid, code) {
|
|
447
|
+
var h = conn._streamHandlers[sid];
|
|
448
|
+
if (h && h.onStopSending) h.onStopSending(code);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
// GOAWAY (RFC 9114 §5.2): requests on streams from its id on were not,
|
|
452
|
+
// and will not be, processed — retryable, as HTTP/2's (h2_client). And
|
|
453
|
+
// the connection's end ends every request still on it. Neither was
|
|
454
|
+
// heard: a request sent as the server's GOAWAY was on its way — the
|
|
455
|
+
// server rejected it and closed, its RESET_STREAM behind the close —
|
|
456
|
+
// waited for a response forever.
|
|
457
|
+
function failAll(fromId, make) {
|
|
458
|
+
for (var sid in conn._streamHandlers) {
|
|
459
|
+
if (Number(sid) < fromId) continue;
|
|
460
|
+
var h = conn._streamHandlers[sid];
|
|
461
|
+
delete conn._streamHandlers[sid];
|
|
462
|
+
if (h.onGone) h.onGone(make(Number(sid)));
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
conn._goawayId = Infinity;
|
|
466
|
+
h3c.on('goaway', function (id) {
|
|
467
|
+
var goawayId = conn._goawayId = Math.min(conn._goawayId, Number(id));
|
|
468
|
+
// For a pool: no new requests here (as an HTTP/2 session's goingAway).
|
|
469
|
+
// The GOAWAY can come before the response that puts the connection in
|
|
470
|
+
// the pool, so the pool asks rather than waits for the event.
|
|
471
|
+
conn.goingAway = true;
|
|
472
|
+
failAll(goawayId, function () {
|
|
473
|
+
var e = new Error('HTTP/3 request not processed before GOAWAY');
|
|
474
|
+
e.code = 'ERR_HTTP3_GOAWAY_UNPROCESSED';
|
|
475
|
+
e.retryable = true;
|
|
476
|
+
return e;
|
|
477
|
+
});
|
|
478
|
+
});
|
|
479
|
+
// On 'closing' — the moment the connection is over (a CONNECTION_CLOSE
|
|
480
|
+
// either way, the idle timeout) — not only on 'close', which waits out
|
|
481
|
+
// the draining period (RFC 9000 §10.2, ~3 s): a response the server cut
|
|
482
|
+
// off hung that long before it failed.
|
|
483
|
+
function connectionOver() {
|
|
484
|
+
failAll(0, function (sid) {
|
|
485
|
+
var e = new Error('HTTP/3 connection closed before the response ended');
|
|
486
|
+
e.code = 'ERR_HTTP3_CONNECTION_CLOSED';
|
|
487
|
+
// Past a GOAWAY's id the server said it did not process it.
|
|
488
|
+
e.retryable = sid >= conn._goawayId;
|
|
489
|
+
return e;
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
conn.quic.on('closing', connectionOver);
|
|
493
|
+
conn.quic.on('close', connectionOver);
|
|
228
494
|
}
|
|
229
495
|
|
|
230
496
|
|
|
@@ -234,11 +500,37 @@ function request(options, callback) {
|
|
|
234
500
|
* routing is now by map key, so no per-listener streamId check is needed.
|
|
235
501
|
*/
|
|
236
502
|
function setupResponseHandlers(conn) {
|
|
503
|
+
respConn = conn;
|
|
504
|
+
// A connection the server is done with — GOAWAY'd below this stream, or
|
|
505
|
+
// gone — takes no request: it fails now, as it would have on the wire.
|
|
506
|
+
// On the next tick: the caller has not attached its listeners yet.
|
|
507
|
+
var state = conn.quic ? conn.quic.state : 'closed';
|
|
508
|
+
if (streamId >= conn._goawayId || state === 'closed' || state === 'closing' || state === 'draining') {
|
|
509
|
+
var gone = new Error(streamId >= conn._goawayId ? 'HTTP/3 request not processed before GOAWAY' : 'HTTP/3 connection closed before the response ended');
|
|
510
|
+
gone.code = streamId >= conn._goawayId ? 'ERR_HTTP3_GOAWAY_UNPROCESSED' : 'ERR_HTTP3_CONNECTION_CLOSED';
|
|
511
|
+
gone.retryable = true;
|
|
512
|
+
process.nextTick(function () { fail(gone); });
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
237
515
|
conn._streamHandlers[streamId] = {
|
|
238
516
|
onHeaders: function (hdrs) {
|
|
517
|
+
// 1xx (103 Early Hints): informational — the response follows on the
|
|
518
|
+
// same stream. node:http's 'information', not the response: taken as
|
|
519
|
+
// it, the request got status 103 and the real head was lost.
|
|
520
|
+
var st1 = parseInt(hdrs[':status'], 10);
|
|
521
|
+
if (st1 >= 100 && st1 < 200) {
|
|
522
|
+
var ih = {}, raw = [];
|
|
523
|
+
for (var hn in hdrs) { if (hn[0] !== ':') { ih[hn] = hdrs[hn]; raw.push(hn, String(hdrs[hn])); } }
|
|
524
|
+
emit(reqListeners, 'information', { statusCode: st1, statusMessage: '', httpVersion: '3.0', httpVersionMajor: 3, httpVersionMinor: 0, headers: ih, rawHeaders: raw });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
// 425 Too Early to what went in 0-RTT (RFC 8470 §5.2): not the
|
|
528
|
+
// response — the request goes again once the handshake is done.
|
|
529
|
+
if (st1 === 425 && earlyBody && !retry425) { retry425 = true; earlyResult = 'rejected'; return; }
|
|
239
530
|
clientRes.headers = hdrs;
|
|
240
531
|
clientRes.statusCode = parseInt(hdrs[':status']) || 0;
|
|
241
532
|
clientRes._headersReceived = true;
|
|
533
|
+
clientRes.earlyData = earlyTried ? earlyResult : null;
|
|
242
534
|
|
|
243
535
|
// Fire callback with response (once)
|
|
244
536
|
if (typeof callback === 'function') {
|
|
@@ -249,62 +541,208 @@ function request(options, callback) {
|
|
|
249
541
|
},
|
|
250
542
|
|
|
251
543
|
onBody: function (data) {
|
|
544
|
+
if (retry425) return;
|
|
252
545
|
emit(clientRes, 'data', data);
|
|
253
546
|
},
|
|
254
547
|
|
|
548
|
+
// The response's trailers: res.trailers / res.rawTrailers, as node:http
|
|
549
|
+
// has them by 'end' (gRPC's grpc-status).
|
|
550
|
+
onTrailers: function (trailers) {
|
|
551
|
+
clientRes.trailers = trailers;
|
|
552
|
+
clientRes.rawTrailers = [];
|
|
553
|
+
for (var tn in trailers) clientRes.rawTrailers.push(tn, String(trailers[tn]));
|
|
554
|
+
},
|
|
555
|
+
|
|
556
|
+
onError: function (code, reason) {
|
|
557
|
+
var err = new Error('HTTP/3 stream error: ' + reason);
|
|
558
|
+
err.code = 'ERR_HTTP3_STREAM_ERROR';
|
|
559
|
+
err.errorCode = code;
|
|
560
|
+
fail(err);
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
onReset: function (code) {
|
|
564
|
+
var name = H3_ERR_NAME[code] || ('0x' + Number(code).toString(16));
|
|
565
|
+
var err = new Error('HTTP/3 stream reset by the server: ' + name);
|
|
566
|
+
err.code = 'ERR_HTTP3_STREAM_RESET';
|
|
567
|
+
err.errorCode = code;
|
|
568
|
+
// H3_REQUEST_REJECTED: the server did not process it (RFC 9114 §4.1.1)
|
|
569
|
+
// — safe to retry, as HTTP/2's REFUSED_STREAM. Any other code is the
|
|
570
|
+
// server's answer, not a failure to reach it: not sent again over TCP.
|
|
571
|
+
err.retryable = (code === 0x10b);
|
|
572
|
+
if (!err.retryable) err.requestError = true;
|
|
573
|
+
fail(err);
|
|
574
|
+
},
|
|
575
|
+
|
|
576
|
+
// H3_NO_ERROR: the server answers without the rest of the body (RFC
|
|
577
|
+
// 9114 §4.1.2) — the response comes, and is not to be discarded. Any
|
|
578
|
+
// other code before a response: the request is refused or cancelled,
|
|
579
|
+
// as by a reset (H3_REQUEST_REJECTED retryable).
|
|
580
|
+
onStopSending: function (code) {
|
|
581
|
+
if (code === 0x100 || clientRes._headersReceived) return;
|
|
582
|
+
var h = conn._streamHandlers[streamId];
|
|
583
|
+
if (h) delete conn._streamHandlers[streamId];
|
|
584
|
+
var name = H3_ERR_NAME[code] || ('0x' + Number(code).toString(16));
|
|
585
|
+
var err = new Error('HTTP/3 request stopped by the server: ' + name);
|
|
586
|
+
err.code = 'ERR_HTTP3_STREAM_RESET';
|
|
587
|
+
err.errorCode = code;
|
|
588
|
+
err.retryable = (code === 0x10b);
|
|
589
|
+
if (!err.retryable) err.requestError = true;
|
|
590
|
+
fail(err);
|
|
591
|
+
},
|
|
592
|
+
|
|
593
|
+
// The connection gone (or past GOAWAY) under it. Sent, not processed
|
|
594
|
+
// per GOAWAY, and not idempotent: the server may have acted on it — not
|
|
595
|
+
// sent again over TCP (RFC 9110 §9.2.2).
|
|
596
|
+
onGone: function (err) {
|
|
597
|
+
if (!err.retryable && requestSent && !({ GET: 1, HEAD: 1, OPTIONS: 1, TRACE: 1, PUT: 1, DELETE: 1 })[method]) err.requestError = true;
|
|
598
|
+
fail(err);
|
|
599
|
+
},
|
|
600
|
+
|
|
255
601
|
onEnd: function () {
|
|
602
|
+
if (retry425 && !clientRes._headersReceived) { againAfter425(); return; }
|
|
603
|
+
finished = true;
|
|
604
|
+
clientRes._releaseBody(); // all of it has arrived: nothing more to wait for
|
|
256
605
|
emit(clientRes, 'end');
|
|
257
|
-
|
|
258
|
-
// Auto-close only if we own the connection (see `autoClose` above).
|
|
259
|
-
if (autoClose) {
|
|
260
|
-
setTimeout(function () {
|
|
261
|
-
if (quic) quic.close(0, 'done');
|
|
262
|
-
if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
263
|
-
}, 100);
|
|
264
|
-
}
|
|
606
|
+
closeIfOwned();
|
|
265
607
|
}
|
|
266
608
|
};
|
|
267
609
|
}
|
|
268
610
|
|
|
611
|
+
// The request failed. Before the response began, the request reports it
|
|
612
|
+
// ('error' — a fallback may still be tried); after, the response does —
|
|
613
|
+
// the request's error was reported to a caller that had moved on to the
|
|
614
|
+
// response, and the response hung.
|
|
615
|
+
function fail(err) {
|
|
616
|
+
if (finished) return;
|
|
617
|
+
finished = true;
|
|
618
|
+
clientRes._releaseBody();
|
|
619
|
+
if (clientRes._headersReceived) emit(clientRes, 'error', err);
|
|
620
|
+
else emit(reqListeners, 'error', err);
|
|
621
|
+
closeIfOwned();
|
|
622
|
+
}
|
|
269
623
|
|
|
270
|
-
|
|
271
|
-
|
|
624
|
+
// Auto-close only if we own the connection (see `autoClose` above) — after
|
|
625
|
+
// the response ends, or fails.
|
|
626
|
+
// An answer to early data can come before the handshake is confirmed,
|
|
627
|
+
// and with it the server's session ticket: the close waits for that, or
|
|
628
|
+
// the next visit would have no ticket to resume (and send early) with.
|
|
629
|
+
function closeIfOwned() {
|
|
630
|
+
if (!autoClose) return;
|
|
631
|
+
if (earlyTried && quic && quic.whenConfirmed) quic.whenConfirmed(function () { setTimeout(closeConnection, 100); });
|
|
632
|
+
else setTimeout(closeConnection, 100);
|
|
633
|
+
}
|
|
272
634
|
|
|
273
|
-
|
|
635
|
+
// RFC 9114 §5.2: closed with an HTTP/3 code in the application space.
|
|
636
|
+
// H3_NO_ERROR. The UDP socket closes when the connection does — its
|
|
637
|
+
// closing period over (quic_socket, on 'close') — not here: closed at once,
|
|
638
|
+
// the CONNECTION_CLOSE just queued on it was cancelled with it, and the
|
|
639
|
+
// server never learned the connection was over. A request with agent:
|
|
640
|
+
// false left the server's connection (and a response it was still
|
|
641
|
+
// writing) up until its idle timeout.
|
|
642
|
+
function closeConnection() {
|
|
643
|
+
if (quic) quic.close(0x0100, 'done', true);
|
|
644
|
+
else if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
645
|
+
}
|
|
274
646
|
|
|
275
|
-
// MUST send control streams (SETTINGS) before any request
|
|
276
|
-
h3.sendControlStreams();
|
|
277
647
|
|
|
278
|
-
|
|
648
|
+
// The stream exists. options.onReady (the unified client) may still turn
|
|
649
|
+
// the request down — another protocol was ready first and carries it:
|
|
650
|
+
// nothing goes out, and 'superseded' says so (the connection stays, for
|
|
651
|
+
// the pool). Otherwise: sent whole if the body is complete, streamed if
|
|
652
|
+
// some of it was written, or on the first write or end().
|
|
653
|
+
function ready(conn) {
|
|
654
|
+
if (typeof options.onReady === 'function' && !options.onReady()) {
|
|
655
|
+
finished = true;
|
|
656
|
+
if (conn && conn._streamHandlers && streamId !== null) delete conn._streamHandlers[streamId];
|
|
657
|
+
// Its stream ID, taken and never used: cancelled, as by destroy()
|
|
658
|
+
// (an unused ID below a later request's is opened by that request).
|
|
659
|
+
if (!autoClose && conn && conn.h3 && streamId !== null) { try { conn.h3.abortStream(streamId, 0x10c); } catch (e) {} }
|
|
660
|
+
// Next tick: on a pooled connection this runs inside request(), before
|
|
661
|
+
// the caller has its listeners on.
|
|
662
|
+
process.nextTick(function () { emit(reqListeners, 'superseded'); });
|
|
663
|
+
if (autoClose && quic) closeConnection();
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
readyDone = true;
|
|
667
|
+
if (reqFinished) sendRequest();
|
|
668
|
+
else if (reqBody.length) startStreaming();
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// contentLength: the body's size when it is known whole (ended before it
|
|
672
|
+
// went) — sent as h2_client and node:https send it, and as browsers do. A
|
|
673
|
+
// server that decides whether there is a body from the headers (Express's
|
|
674
|
+
// body-parser: content-length or transfer-encoding) saw none over HTTP/3,
|
|
675
|
+
// and req.body came out empty.
|
|
676
|
+
function requestHeaders(contentLength) {
|
|
279
677
|
var h3Headers = {
|
|
280
678
|
':method': method,
|
|
281
679
|
':scheme': 'https',
|
|
282
680
|
':authority': hostname + (port !== 443 ? ':' + port : ''),
|
|
283
681
|
':path': path
|
|
284
682
|
};
|
|
285
|
-
|
|
286
|
-
//
|
|
683
|
+
// A repeated field (an array) stays one line per value: String(list)
|
|
684
|
+
// made two cookies "a=1,b=2", which the server reads as one cookie.
|
|
287
685
|
for (var name in headers) {
|
|
288
|
-
|
|
686
|
+
var v = headers[name];
|
|
687
|
+
h3Headers[name.toLowerCase()] = Array.isArray(v) ? v.map(String) : String(v);
|
|
289
688
|
}
|
|
290
|
-
|
|
291
|
-
// Default headers
|
|
292
689
|
if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
|
|
293
690
|
if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
|
|
294
691
|
if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
|
|
692
|
+
if (contentLength != null && h3Headers['content-length'] === undefined) h3Headers['content-length'] = String(contentLength);
|
|
693
|
+
return h3Headers;
|
|
694
|
+
}
|
|
295
695
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
696
|
+
// The request whole: ended before it could go — headers, the body, the
|
|
697
|
+
// trailers; FIN on the last of them.
|
|
698
|
+
function sendRequest() {
|
|
699
|
+
if (!h3 || !connected || cancelled || finished || requestSent) return;
|
|
700
|
+
requestSent = true;
|
|
701
|
+
if (DEBUG) console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
702
|
+
h3.sendControlStreams(); // SETTINGS before any request
|
|
703
|
+
var body = reqBody; reqBody = []; reqBodyBytes = 0;
|
|
704
|
+
// Early: kept, to go again if the server refuses it.
|
|
705
|
+
if (earlyTried && !handshaken) earlyBody = { body: body.slice(), trailers: reqTrailers };
|
|
706
|
+
if (body.length === 0 && !reqTrailers) { h3.sendHeaders(streamId, requestHeaders(), true); return; }
|
|
707
|
+
var bodyLength = 0;
|
|
708
|
+
for (var bi = 0; bi < body.length; bi++) bodyLength += body[bi].byteLength;
|
|
709
|
+
h3.sendHeaders(streamId, requestHeaders(body.length ? bodyLength : null), false);
|
|
710
|
+
for (var i = 0; i < body.length; i++) h3.sendBody(streamId, body[i], i === body.length - 1 && !reqTrailers);
|
|
711
|
+
if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// The body streamed: HEADERS now, without FIN, then what was held.
|
|
715
|
+
function startStreaming() {
|
|
716
|
+
if (!h3 || cancelled || finished || requestSent) return;
|
|
717
|
+
requestSent = true;
|
|
718
|
+
streaming = true;
|
|
719
|
+
if (DEBUG) console.log('[client] streaming request method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
720
|
+
h3.sendControlStreams();
|
|
721
|
+
h3.sendHeaders(streamId, requestHeaders(), false);
|
|
722
|
+
var held = reqBody; reqBody = []; reqBodyBytes = 0;
|
|
723
|
+
var more = true;
|
|
724
|
+
for (var i = 0; i < held.length; i++) more = streamOut(held[i]);
|
|
725
|
+
if (more && needDrain) { needDrain = false; emit(reqListeners, 'drain'); }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// One piece of a streamed body. Returns whether more may be written now:
|
|
729
|
+
// false while more than BODY_HIGH_WATER of the stream is not yet sent,
|
|
730
|
+
// and 'drain' when it is (the stream's send side, whenStreamSent).
|
|
731
|
+
function streamOut(chunk) {
|
|
732
|
+
h3.sendBody(streamId, chunk, false);
|
|
733
|
+
if (drainWaiting) { needDrain = true; return false; }
|
|
734
|
+
var later = false, over = true;
|
|
735
|
+
quic.whenStreamSent(streamId, BODY_HIGH_WATER, function () {
|
|
736
|
+
over = false;
|
|
737
|
+
if (!later) return;
|
|
738
|
+
drainWaiting = false;
|
|
739
|
+
if (needDrain) { needDrain = false; emit(reqListeners, 'drain'); }
|
|
740
|
+
});
|
|
741
|
+
later = true;
|
|
742
|
+
if (!over) return true;
|
|
743
|
+
drainWaiting = true;
|
|
744
|
+
needDrain = true;
|
|
745
|
+
return false;
|
|
308
746
|
}
|
|
309
747
|
|
|
310
748
|
|