quico 0.3.0 → 0.7.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 +2489 -147
- package/index.js +56 -34
- package/package.json +3 -3
- package/src/agent.js +364 -36
- package/src/client.js +474 -118
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +257 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -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 +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +476 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6666 -632
- package/src/quic_server.js +822 -69
- package/src/quic_socket.js +183 -7
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +376 -36
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +516 -75
- package/src/tls_options.js +108 -0
- package/src/transport.js +541 -39
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -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
|
// ============================================================
|
|
@@ -34,6 +41,18 @@ function request(options, callback) {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
var hostname = options.hostname || options.host || 'localhost';
|
|
44
|
+
// Certificate verification options, forwarded to the QUIC/TLS layer.
|
|
45
|
+
// Defaults to verifying (node:tls semantics); without this passthrough the
|
|
46
|
+
// option dies here and H3 connections can never opt out — which makes the
|
|
47
|
+
// one transport this library exists for the only one that can't reach a
|
|
48
|
+
// self-signed or private-CA server (H2/H1.1 fallbacks honour the option).
|
|
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'); })();
|
|
55
|
+
var ca = options.ca || null;
|
|
37
56
|
var port = options.port || 443;
|
|
38
57
|
var path = options.path || '/';
|
|
39
58
|
var method = (options.method || 'GET').toUpperCase();
|
|
@@ -58,6 +77,28 @@ function request(options, callback) {
|
|
|
58
77
|
var udpSocket = existingConn ? existingConn.udpSocket : null;
|
|
59
78
|
var streamId = null; // assigned when connection is ready
|
|
60
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
|
|
61
102
|
|
|
62
103
|
var clientReq = {
|
|
63
104
|
on: function (event, fn) {
|
|
@@ -65,22 +106,70 @@ function request(options, callback) {
|
|
|
65
106
|
return clientReq;
|
|
66
107
|
},
|
|
67
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.
|
|
68
113
|
write: function (chunk) {
|
|
114
|
+
if (finished || cancelled || reqFinished) return false;
|
|
69
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);
|
|
70
119
|
reqBody.push(chunk);
|
|
120
|
+
reqBodyBytes += chunk.byteLength;
|
|
121
|
+
if (reqBodyBytes >= BODY_HIGH_WATER) { needDrain = true; return false; }
|
|
71
122
|
return true;
|
|
72
123
|
},
|
|
73
124
|
|
|
74
|
-
|
|
75
|
-
|
|
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; }
|
|
76
148
|
reqFinished = true;
|
|
77
|
-
if (
|
|
149
|
+
if (readyDone) sendRequest();
|
|
78
150
|
return clientReq;
|
|
79
151
|
},
|
|
80
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.
|
|
81
158
|
destroy: function () {
|
|
82
|
-
if (
|
|
83
|
-
|
|
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
|
|
84
173
|
}
|
|
85
174
|
};
|
|
86
175
|
|
|
@@ -88,14 +177,26 @@ function request(options, callback) {
|
|
|
88
177
|
var clientRes = {
|
|
89
178
|
statusCode: 0,
|
|
90
179
|
headers: {},
|
|
180
|
+
trailers: {},
|
|
181
|
+
rawTrailers: [],
|
|
91
182
|
_listeners: {},
|
|
92
183
|
_headersReceived: false,
|
|
93
184
|
|
|
94
185
|
on: function (event, fn) {
|
|
95
186
|
(clientRes._listeners[event] = clientRes._listeners[event] || []).push(fn);
|
|
96
187
|
return clientRes;
|
|
97
|
-
}
|
|
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); }
|
|
98
198
|
};
|
|
199
|
+
var respConn = null; // the connection the response comes on (setupResponseHandlers)
|
|
99
200
|
|
|
100
201
|
function emit(obj, event, data) {
|
|
101
202
|
var listeners = (obj._listeners || obj)[event] || [];
|
|
@@ -115,7 +216,7 @@ function request(options, callback) {
|
|
|
115
216
|
streamId = allocateStreamId(existingConn);
|
|
116
217
|
installSharedHandlers(existingConn);
|
|
117
218
|
setupResponseHandlers(existingConn);
|
|
118
|
-
|
|
219
|
+
ready(existingConn);
|
|
119
220
|
} else {
|
|
120
221
|
resolveAndConnect(hostname, port);
|
|
121
222
|
}
|
|
@@ -140,18 +241,122 @@ function request(options, callback) {
|
|
|
140
241
|
|
|
141
242
|
|
|
142
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');
|
|
143
248
|
udpSocket = createQuicClientSocket({
|
|
144
249
|
remoteIp: remoteIp,
|
|
145
250
|
remotePort: remotePort,
|
|
146
|
-
|
|
251
|
+
// SNI and the name the certificate is checked for: tls.connect's
|
|
252
|
+
// servername where given.
|
|
253
|
+
hostname: options.servername || hostname,
|
|
254
|
+
rejectUnauthorized: rejectUnauthorized,
|
|
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
|
+
},
|
|
147
299
|
|
|
148
300
|
onError: function (err) {
|
|
301
|
+
if (cancelled) return;
|
|
149
302
|
emit(reqListeners, 'error', err);
|
|
150
303
|
},
|
|
151
304
|
|
|
152
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 () {
|
|
153
354
|
quic = q;
|
|
154
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; }
|
|
155
360
|
if (DEBUG) console.log('[client] QUIC connected');
|
|
156
361
|
|
|
157
362
|
h3 = new H3Connection({ quicConnection: quic, isServer: false });
|
|
@@ -169,12 +374,8 @@ function request(options, callback) {
|
|
|
169
374
|
clientReq._connection = conn;
|
|
170
375
|
|
|
171
376
|
setupResponseHandlers(conn);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if (reqFinished) sendRequest();
|
|
175
|
-
}
|
|
176
|
-
// onClose: the helper closes the UDP socket; nothing extra needed here.
|
|
177
|
-
});
|
|
377
|
+
ready(conn);
|
|
378
|
+
})();
|
|
178
379
|
}
|
|
179
380
|
|
|
180
381
|
|
|
@@ -210,12 +411,80 @@ function request(options, callback) {
|
|
|
210
411
|
var h = conn._streamHandlers[sid];
|
|
211
412
|
if (h) h.onBody(data);
|
|
212
413
|
});
|
|
414
|
+
h3c.on('http_trailers', function (sid, trailers) {
|
|
415
|
+
var h = conn._streamHandlers[sid];
|
|
416
|
+
if (h && h.onTrailers) h.onTrailers(trailers);
|
|
417
|
+
});
|
|
213
418
|
h3c.on('http_end', function (sid) {
|
|
214
419
|
var h = conn._streamHandlers[sid];
|
|
215
420
|
if (!h) return;
|
|
216
421
|
delete conn._streamHandlers[sid]; // free per-stream state before dispatch
|
|
217
422
|
h.onEnd();
|
|
218
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
|
+
conn.quic.on('close', function () {
|
|
480
|
+
failAll(0, function (sid) {
|
|
481
|
+
var e = new Error('HTTP/3 connection closed before the response ended');
|
|
482
|
+
e.code = 'ERR_HTTP3_CONNECTION_CLOSED';
|
|
483
|
+
// Past a GOAWAY's id the server said it did not process it.
|
|
484
|
+
e.retryable = sid >= conn._goawayId;
|
|
485
|
+
return e;
|
|
486
|
+
});
|
|
487
|
+
});
|
|
219
488
|
}
|
|
220
489
|
|
|
221
490
|
|
|
@@ -225,11 +494,37 @@ function request(options, callback) {
|
|
|
225
494
|
* routing is now by map key, so no per-listener streamId check is needed.
|
|
226
495
|
*/
|
|
227
496
|
function setupResponseHandlers(conn) {
|
|
497
|
+
respConn = conn;
|
|
498
|
+
// A connection the server is done with — GOAWAY'd below this stream, or
|
|
499
|
+
// gone — takes no request: it fails now, as it would have on the wire.
|
|
500
|
+
// On the next tick: the caller has not attached its listeners yet.
|
|
501
|
+
var state = conn.quic ? conn.quic.state : 'closed';
|
|
502
|
+
if (streamId >= conn._goawayId || state === 'closed' || state === 'closing' || state === 'draining') {
|
|
503
|
+
var gone = new Error(streamId >= conn._goawayId ? 'HTTP/3 request not processed before GOAWAY' : 'HTTP/3 connection closed before the response ended');
|
|
504
|
+
gone.code = streamId >= conn._goawayId ? 'ERR_HTTP3_GOAWAY_UNPROCESSED' : 'ERR_HTTP3_CONNECTION_CLOSED';
|
|
505
|
+
gone.retryable = true;
|
|
506
|
+
process.nextTick(function () { fail(gone); });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
228
509
|
conn._streamHandlers[streamId] = {
|
|
229
510
|
onHeaders: function (hdrs) {
|
|
511
|
+
// 1xx (103 Early Hints): informational — the response follows on the
|
|
512
|
+
// same stream. node:http's 'information', not the response: taken as
|
|
513
|
+
// it, the request got status 103 and the real head was lost.
|
|
514
|
+
var st1 = parseInt(hdrs[':status'], 10);
|
|
515
|
+
if (st1 >= 100 && st1 < 200) {
|
|
516
|
+
var ih = {}, raw = [];
|
|
517
|
+
for (var hn in hdrs) { if (hn[0] !== ':') { ih[hn] = hdrs[hn]; raw.push(hn, String(hdrs[hn])); } }
|
|
518
|
+
emit(reqListeners, 'information', { statusCode: st1, statusMessage: '', httpVersion: '3.0', httpVersionMajor: 3, httpVersionMinor: 0, headers: ih, rawHeaders: raw });
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// 425 Too Early to what went in 0-RTT (RFC 8470 §5.2): not the
|
|
522
|
+
// response — the request goes again once the handshake is done.
|
|
523
|
+
if (st1 === 425 && earlyBody && !retry425) { retry425 = true; earlyResult = 'rejected'; return; }
|
|
230
524
|
clientRes.headers = hdrs;
|
|
231
525
|
clientRes.statusCode = parseInt(hdrs[':status']) || 0;
|
|
232
526
|
clientRes._headersReceived = true;
|
|
527
|
+
clientRes.earlyData = earlyTried ? earlyResult : null;
|
|
233
528
|
|
|
234
529
|
// Fire callback with response (once)
|
|
235
530
|
if (typeof callback === 'function') {
|
|
@@ -240,62 +535,195 @@ function request(options, callback) {
|
|
|
240
535
|
},
|
|
241
536
|
|
|
242
537
|
onBody: function (data) {
|
|
538
|
+
if (retry425) return;
|
|
243
539
|
emit(clientRes, 'data', data);
|
|
244
540
|
},
|
|
245
541
|
|
|
542
|
+
// The response's trailers: res.trailers / res.rawTrailers, as node:http
|
|
543
|
+
// has them by 'end' (gRPC's grpc-status).
|
|
544
|
+
onTrailers: function (trailers) {
|
|
545
|
+
clientRes.trailers = trailers;
|
|
546
|
+
clientRes.rawTrailers = [];
|
|
547
|
+
for (var tn in trailers) clientRes.rawTrailers.push(tn, String(trailers[tn]));
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
onError: function (code, reason) {
|
|
551
|
+
var err = new Error('HTTP/3 stream error: ' + reason);
|
|
552
|
+
err.code = 'ERR_HTTP3_STREAM_ERROR';
|
|
553
|
+
err.errorCode = code;
|
|
554
|
+
fail(err);
|
|
555
|
+
},
|
|
556
|
+
|
|
557
|
+
onReset: function (code) {
|
|
558
|
+
var name = H3_ERR_NAME[code] || ('0x' + Number(code).toString(16));
|
|
559
|
+
var err = new Error('HTTP/3 stream reset by the server: ' + name);
|
|
560
|
+
err.code = 'ERR_HTTP3_STREAM_RESET';
|
|
561
|
+
err.errorCode = code;
|
|
562
|
+
// H3_REQUEST_REJECTED: the server did not process it (RFC 9114 §4.1.1)
|
|
563
|
+
// — safe to retry, as HTTP/2's REFUSED_STREAM. Any other code is the
|
|
564
|
+
// server's answer, not a failure to reach it: not sent again over TCP.
|
|
565
|
+
err.retryable = (code === 0x10b);
|
|
566
|
+
if (!err.retryable) err.requestError = true;
|
|
567
|
+
fail(err);
|
|
568
|
+
},
|
|
569
|
+
|
|
570
|
+
// H3_NO_ERROR: the server answers without the rest of the body (RFC
|
|
571
|
+
// 9114 §4.1.2) — the response comes, and is not to be discarded. Any
|
|
572
|
+
// other code before a response: the request is refused or cancelled,
|
|
573
|
+
// as by a reset (H3_REQUEST_REJECTED retryable).
|
|
574
|
+
onStopSending: function (code) {
|
|
575
|
+
if (code === 0x100 || clientRes._headersReceived) return;
|
|
576
|
+
var h = conn._streamHandlers[streamId];
|
|
577
|
+
if (h) delete conn._streamHandlers[streamId];
|
|
578
|
+
var name = H3_ERR_NAME[code] || ('0x' + Number(code).toString(16));
|
|
579
|
+
var err = new Error('HTTP/3 request stopped by the server: ' + name);
|
|
580
|
+
err.code = 'ERR_HTTP3_STREAM_RESET';
|
|
581
|
+
err.errorCode = code;
|
|
582
|
+
err.retryable = (code === 0x10b);
|
|
583
|
+
if (!err.retryable) err.requestError = true;
|
|
584
|
+
fail(err);
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
// The connection gone (or past GOAWAY) under it. Sent, not processed
|
|
588
|
+
// per GOAWAY, and not idempotent: the server may have acted on it — not
|
|
589
|
+
// sent again over TCP (RFC 9110 §9.2.2).
|
|
590
|
+
onGone: function (err) {
|
|
591
|
+
if (!err.retryable && requestSent && !({ GET: 1, HEAD: 1, OPTIONS: 1, TRACE: 1, PUT: 1, DELETE: 1 })[method]) err.requestError = true;
|
|
592
|
+
fail(err);
|
|
593
|
+
},
|
|
594
|
+
|
|
246
595
|
onEnd: function () {
|
|
596
|
+
if (retry425 && !clientRes._headersReceived) { againAfter425(); return; }
|
|
597
|
+
finished = true;
|
|
598
|
+
clientRes._releaseBody(); // all of it has arrived: nothing more to wait for
|
|
247
599
|
emit(clientRes, 'end');
|
|
248
|
-
|
|
249
|
-
// Auto-close only if we own the connection (see `autoClose` above).
|
|
250
|
-
if (autoClose) {
|
|
251
|
-
setTimeout(function () {
|
|
252
|
-
if (quic) quic.close(0, 'done');
|
|
253
|
-
if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
254
|
-
}, 100);
|
|
255
|
-
}
|
|
600
|
+
closeIfOwned();
|
|
256
601
|
}
|
|
257
602
|
};
|
|
258
603
|
}
|
|
259
604
|
|
|
605
|
+
// The request failed. Before the response began, the request reports it
|
|
606
|
+
// ('error' — a fallback may still be tried); after, the response does —
|
|
607
|
+
// the request's error was reported to a caller that had moved on to the
|
|
608
|
+
// response, and the response hung.
|
|
609
|
+
function fail(err) {
|
|
610
|
+
if (finished) return;
|
|
611
|
+
finished = true;
|
|
612
|
+
clientRes._releaseBody();
|
|
613
|
+
if (clientRes._headersReceived) emit(clientRes, 'error', err);
|
|
614
|
+
else emit(reqListeners, 'error', err);
|
|
615
|
+
closeIfOwned();
|
|
616
|
+
}
|
|
260
617
|
|
|
261
|
-
|
|
262
|
-
|
|
618
|
+
// Auto-close only if we own the connection (see `autoClose` above) — after
|
|
619
|
+
// the response ends, or fails.
|
|
620
|
+
// An answer to early data can come before the handshake is confirmed,
|
|
621
|
+
// and with it the server's session ticket: the close waits for that, or
|
|
622
|
+
// the next visit would have no ticket to resume (and send early) with.
|
|
623
|
+
function closeIfOwned() {
|
|
624
|
+
if (!autoClose) return;
|
|
625
|
+
if (earlyTried && quic && quic.whenConfirmed) quic.whenConfirmed(function () { setTimeout(closeConnection, 100); });
|
|
626
|
+
else setTimeout(closeConnection, 100);
|
|
627
|
+
}
|
|
263
628
|
|
|
264
|
-
|
|
629
|
+
// RFC 9114 §5.2: closed with an HTTP/3 code in the application space.
|
|
630
|
+
// H3_NO_ERROR. The UDP socket closes when the connection does — its
|
|
631
|
+
// closing period over (quic_socket, on 'close') — not here: closed at once,
|
|
632
|
+
// the CONNECTION_CLOSE just queued on it was cancelled with it, and the
|
|
633
|
+
// server never learned the connection was over. A request with agent:
|
|
634
|
+
// false left the server's connection (and a response it was still
|
|
635
|
+
// writing) up until its idle timeout.
|
|
636
|
+
function closeConnection() {
|
|
637
|
+
if (quic) quic.close(0x0100, 'done', true);
|
|
638
|
+
else if (udpSocket) { try { udpSocket.close(); } catch (e) {} }
|
|
639
|
+
}
|
|
265
640
|
|
|
266
|
-
// MUST send control streams (SETTINGS) before any request
|
|
267
|
-
h3.sendControlStreams();
|
|
268
641
|
|
|
269
|
-
|
|
642
|
+
// The stream exists. options.onReady (the unified client) may still turn
|
|
643
|
+
// the request down — another protocol was ready first and carries it:
|
|
644
|
+
// nothing goes out, and 'superseded' says so (the connection stays, for
|
|
645
|
+
// the pool). Otherwise: sent whole if the body is complete, streamed if
|
|
646
|
+
// some of it was written, or on the first write or end().
|
|
647
|
+
function ready(conn) {
|
|
648
|
+
if (typeof options.onReady === 'function' && !options.onReady()) {
|
|
649
|
+
finished = true;
|
|
650
|
+
if (conn && conn._streamHandlers && streamId !== null) delete conn._streamHandlers[streamId];
|
|
651
|
+
// Its stream ID, taken and never used: cancelled, as by destroy()
|
|
652
|
+
// (an unused ID below a later request's is opened by that request).
|
|
653
|
+
if (!autoClose && conn && conn.h3 && streamId !== null) { try { conn.h3.abortStream(streamId, 0x10c); } catch (e) {} }
|
|
654
|
+
// Next tick: on a pooled connection this runs inside request(), before
|
|
655
|
+
// the caller has its listeners on.
|
|
656
|
+
process.nextTick(function () { emit(reqListeners, 'superseded'); });
|
|
657
|
+
if (autoClose && quic) closeConnection();
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
readyDone = true;
|
|
661
|
+
if (reqFinished) sendRequest();
|
|
662
|
+
else if (reqBody.length) startStreaming();
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function requestHeaders() {
|
|
270
666
|
var h3Headers = {
|
|
271
667
|
':method': method,
|
|
272
668
|
':scheme': 'https',
|
|
273
669
|
':authority': hostname + (port !== 443 ? ':' + port : ''),
|
|
274
670
|
':path': path
|
|
275
671
|
};
|
|
276
|
-
|
|
277
|
-
// Add user headers
|
|
278
|
-
for (var name in headers) {
|
|
279
|
-
h3Headers[name.toLowerCase()] = String(headers[name]);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Default headers
|
|
672
|
+
for (var name in headers) h3Headers[name.toLowerCase()] = String(headers[name]);
|
|
283
673
|
if (!h3Headers['user-agent']) h3Headers['user-agent'] = 'quico/1.0';
|
|
284
674
|
if (!h3Headers['accept']) h3Headers['accept'] = '*/*';
|
|
285
675
|
if (!h3Headers['accept-encoding']) h3Headers['accept-encoding'] = 'identity';
|
|
676
|
+
return h3Headers;
|
|
677
|
+
}
|
|
286
678
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
679
|
+
// The request whole: ended before it could go — headers, the body, the
|
|
680
|
+
// trailers; FIN on the last of them.
|
|
681
|
+
function sendRequest() {
|
|
682
|
+
if (!h3 || !connected || cancelled || finished || requestSent) return;
|
|
683
|
+
requestSent = true;
|
|
684
|
+
if (DEBUG) console.log('[client] sendRequest method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
685
|
+
h3.sendControlStreams(); // SETTINGS before any request
|
|
686
|
+
var body = reqBody; reqBody = []; reqBodyBytes = 0;
|
|
687
|
+
// Early: kept, to go again if the server refuses it.
|
|
688
|
+
if (earlyTried && !handshaken) earlyBody = { body: body.slice(), trailers: reqTrailers };
|
|
689
|
+
if (body.length === 0 && !reqTrailers) { h3.sendHeaders(streamId, requestHeaders(), true); return; }
|
|
690
|
+
h3.sendHeaders(streamId, requestHeaders(), false);
|
|
691
|
+
for (var i = 0; i < body.length; i++) h3.sendBody(streamId, body[i], i === body.length - 1 && !reqTrailers);
|
|
692
|
+
if (reqTrailers) h3.sendHeaders(streamId, reqTrailers, true);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// The body streamed: HEADERS now, without FIN, then what was held.
|
|
696
|
+
function startStreaming() {
|
|
697
|
+
if (!h3 || cancelled || finished || requestSent) return;
|
|
698
|
+
requestSent = true;
|
|
699
|
+
streaming = true;
|
|
700
|
+
if (DEBUG) console.log('[client] streaming request method=' + method + ' path=' + path + ' stream=' + streamId);
|
|
701
|
+
h3.sendControlStreams();
|
|
702
|
+
h3.sendHeaders(streamId, requestHeaders(), false);
|
|
703
|
+
var held = reqBody; reqBody = []; reqBodyBytes = 0;
|
|
704
|
+
var more = true;
|
|
705
|
+
for (var i = 0; i < held.length; i++) more = streamOut(held[i]);
|
|
706
|
+
if (more && needDrain) { needDrain = false; emit(reqListeners, 'drain'); }
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// One piece of a streamed body. Returns whether more may be written now:
|
|
710
|
+
// false while more than BODY_HIGH_WATER of the stream is not yet sent,
|
|
711
|
+
// and 'drain' when it is (the stream's send side, whenStreamSent).
|
|
712
|
+
function streamOut(chunk) {
|
|
713
|
+
h3.sendBody(streamId, chunk, false);
|
|
714
|
+
if (drainWaiting) { needDrain = true; return false; }
|
|
715
|
+
var later = false, over = true;
|
|
716
|
+
quic.whenStreamSent(streamId, BODY_HIGH_WATER, function () {
|
|
717
|
+
over = false;
|
|
718
|
+
if (!later) return;
|
|
719
|
+
drainWaiting = false;
|
|
720
|
+
if (needDrain) { needDrain = false; emit(reqListeners, 'drain'); }
|
|
721
|
+
});
|
|
722
|
+
later = true;
|
|
723
|
+
if (!over) return true;
|
|
724
|
+
drainWaiting = true;
|
|
725
|
+
needDrain = true;
|
|
726
|
+
return false;
|
|
299
727
|
}
|
|
300
728
|
|
|
301
729
|
|