quico 0.4.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 +1768 -155
- package/index.js +55 -36
- package/package.json +3 -3
- 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 +770 -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 +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 +467 -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 +6517 -657
- package/src/quic_server.js +768 -73
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +361 -35
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +474 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- 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/client.js
CHANGED
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import nodeHttps from 'node:https';
|
|
16
|
-
import nodeHttp2 from 'node:http2';
|
|
17
15
|
import { URL } from 'node:url';
|
|
16
|
+
import lemonTls from 'lemon-tls';
|
|
18
17
|
|
|
19
18
|
import { request as h3Request } from './h3_client.js';
|
|
19
|
+
import { request as h2Request, H2Pool } from './h2_client.js';
|
|
20
|
+
import { request as h1Request, H1Pool } from './h1_client.js';
|
|
20
21
|
import { ClientRequest, IncomingMessage } from './streams.js';
|
|
21
|
-
import {
|
|
22
|
+
import { creditAsRead } from './utils.js';
|
|
23
|
+
import { globalAgent, tlsIdentity } from './agent.js';
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
// ============================================================
|
|
@@ -32,6 +34,37 @@ import { globalAgent } from './agent.js';
|
|
|
32
34
|
// options.http1 (default: agent.http1 or true)
|
|
33
35
|
// ============================================================
|
|
34
36
|
|
|
37
|
+
/**
|
|
38
|
+
* The agent's H2 and H1 pools, created on first use.
|
|
39
|
+
*
|
|
40
|
+
* They are deliberately different objects with different shapes — see the note
|
|
41
|
+
* at the top of h2_client.js. H2 maps an origin to ONE session and gets
|
|
42
|
+
* concurrency from streams; H1 maps an origin to a LIST of sockets and gets
|
|
43
|
+
* concurrency from more sockets. agent.js already kept _h2Pool and _h3Pool
|
|
44
|
+
* separate for the same reason.
|
|
45
|
+
*/
|
|
46
|
+
function agentH2Pool(agent) {
|
|
47
|
+
if (!agent._h2ClientPool) agent._h2ClientPool = new H2Pool({});
|
|
48
|
+
return agent._h2ClientPool;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function agentH1Pool(agent) {
|
|
52
|
+
if (!agent._h1ClientPool) {
|
|
53
|
+
// node:http.Agent's options, with Node's meanings. keepAliveMsecs used to
|
|
54
|
+
// be passed as the idle timeout; in Node it is the TCP keep-alive delay,
|
|
55
|
+
// and how long a free socket is kept is the agent's `timeout`.
|
|
56
|
+
agent._h1ClientPool = new H1Pool({
|
|
57
|
+
maxSockets: agent.maxSockets,
|
|
58
|
+
keepAlive: agent.keepAlive,
|
|
59
|
+
maxFreeSockets: agent.maxFreeSockets,
|
|
60
|
+
keepAliveMsecs: agent.keepAliveMsecs,
|
|
61
|
+
idleTimeout: agent.timeout != null ? agent.timeout : undefined
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return agent._h1ClientPool;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
35
68
|
function request(url, options, callback) {
|
|
36
69
|
|
|
37
70
|
// ---- Normalize arguments (same overloads as node:https) ----
|
|
@@ -72,6 +105,56 @@ function request(url, options, callback) {
|
|
|
72
105
|
var path = options.path || '/';
|
|
73
106
|
var headers = options.headers || {};
|
|
74
107
|
var agent = options.agent !== undefined ? options.agent : globalAgent;
|
|
108
|
+
// The agent's connections, to reuse — those opened with this request's TLS
|
|
109
|
+
// identity (ca, client certificate, rejectUnauthorized, servername), as
|
|
110
|
+
// node:https names them (agent.js tlsIdentity): a connection opened without
|
|
111
|
+
// verification, or with another certificate, does not carry this one.
|
|
112
|
+
// The key-exchange groups to offer (tls.connect's ecdhCurve): the
|
|
113
|
+
// request's, or its agent's. Checked now — a name not implemented throws
|
|
114
|
+
// here, as tls.connect does — and part of which connections it may share.
|
|
115
|
+
if (options.ecdhCurve == null && agent && agent.ecdhCurve != null) options = Object.assign({}, options, { ecdhCurve: agent.ecdhCurve });
|
|
116
|
+
if (options.ecdhCurve != null) lemonTls.parseEcdhCurve(options.ecdhCurve);
|
|
117
|
+
// The signature schemes to offer (tls.connect's sigalgs), the same way.
|
|
118
|
+
if (options.sigalgs == null && agent && agent.sigalgs != null) options = Object.assign({}, options, { sigalgs: agent.sigalgs });
|
|
119
|
+
if (options.sigalgs != null) lemonTls.parseSigalgs(options.sigalgs);
|
|
120
|
+
var pool = agent;
|
|
121
|
+
var poolHost = hostname + '#' + tlsIdentity(options);
|
|
122
|
+
|
|
123
|
+
// The TLS session to resume with on a new connection over `transport`
|
|
124
|
+
// ('quic' or 'tcp'): the caller's `session` (https.request's), or the
|
|
125
|
+
// agent's latest for this origin — and the agent keeps each new one the
|
|
126
|
+
// server sends (the request's 'session' event), as node:https.Agent does.
|
|
127
|
+
// A session the handshake then fails with is dropped (Node's
|
|
128
|
+
// _evictSession), so the next connection starts afresh.
|
|
129
|
+
//
|
|
130
|
+
// Taken from the agent only when a connection is actually made (the
|
|
131
|
+
// getter, read by the transport at connect): a request on a pooled
|
|
132
|
+
// connection uses none, and each session is used once (agent.js). TCP's
|
|
133
|
+
// are kept per ALPN — 'tcp:h2', 'tcp:http/1.1' — since each path offers
|
|
134
|
+
// one, and a ticket can carry 0-RTT only for the protocol it was issued
|
|
135
|
+
// under: an HTTP/2 ticket offered to the HTTP/1.1 path resumed without it.
|
|
136
|
+
// Each session the server sends is also the request's 'session' event, as
|
|
137
|
+
// on node:https.
|
|
138
|
+
function tlsSession(transport, sessPort) {
|
|
139
|
+
var key = (pool && typeof pool._sessionKey === 'function') ? pool._sessionKey(transport, hostname, sessPort, options) : null;
|
|
140
|
+
var taken = false, offered;
|
|
141
|
+
function take() {
|
|
142
|
+
if (options.session) return options.session;
|
|
143
|
+
if (!taken) { taken = true; offered = key ? (pool._takeSession ? pool._takeSession(key) : pool._getSession(key)) : undefined; }
|
|
144
|
+
return offered;
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
// Defines `session` on the transport's options, read at connect.
|
|
148
|
+
into: function (o) { Object.defineProperty(o, 'session', { configurable: true, enumerable: true, get: take }); return o; },
|
|
149
|
+
keep: function (req) {
|
|
150
|
+
req.on('session', function (s) {
|
|
151
|
+
if (key) pool._cacheSession(key, s);
|
|
152
|
+
clientReq.emit('session', s);
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
failed: function () { if (key && taken && offered && !options.session) pool._evictSession(key); }
|
|
156
|
+
};
|
|
157
|
+
}
|
|
75
158
|
|
|
76
159
|
// ---- Resolve protocol flags: options → agent → true ----
|
|
77
160
|
var enableH2 = options.http2 !== undefined ? options.http2
|
|
@@ -85,6 +168,31 @@ function request(url, options, callback) {
|
|
|
85
168
|
|| (agent && agent.h3Timeout)
|
|
86
169
|
|| 3000;
|
|
87
170
|
|
|
171
|
+
// ---- Encrypted Client Hello (RFC 9849) ----
|
|
172
|
+
// options.ech: the server's ECHConfigList (bytes, or base64 — the `ech=`
|
|
173
|
+
// of its DNS HTTPS record): the real ClientHello, with the name, goes
|
|
174
|
+
// encrypted, on every version. Rejected by a server that sent new keys,
|
|
175
|
+
// the request is tried once more with them (it was never sent: ECH fails
|
|
176
|
+
// before the handshake completes). Without a config, GREASE ECH — as the
|
|
177
|
+
// browsers send, so that a client with one does not stand out — unless
|
|
178
|
+
// echGrease is false.
|
|
179
|
+
//
|
|
180
|
+
// The keys a server sent on rejecting the caller's (retry_configs) are
|
|
181
|
+
// kept by the agent (_getEchRetry) and used first on the next requests
|
|
182
|
+
// given the same config: a server that rotated its keys cost a rejection
|
|
183
|
+
// and a retry on every connection until the caller's DNS had the new
|
|
184
|
+
// ones. Kept keys rejected in their turn are forgotten, and the caller's
|
|
185
|
+
// config tried again — whose rejection may bring new keys; a rejection of
|
|
186
|
+
// a connection made with retry_configs never does (RFC 9849 §6.1.6:
|
|
187
|
+
// "Clients SHOULD NOT accept retry_config in response to a connection
|
|
188
|
+
// initiated in response to a retry_config").
|
|
189
|
+
var echOriginal = options.ech || null;
|
|
190
|
+
var echKey = (echOriginal && agent && typeof agent._getEchRetry === 'function') ? agent._sessionKey('ech', hostname, port, options) : null;
|
|
191
|
+
var echKept = echKey ? agent._getEchRetry(echKey) : null;
|
|
192
|
+
var echConfig = echKept || echOriginal;
|
|
193
|
+
var echRetried = false;
|
|
194
|
+
var echGrease = !echConfig && options.echGrease !== false;
|
|
195
|
+
|
|
88
196
|
// ---- Create ClientRequest (returned immediately) ----
|
|
89
197
|
var clientReq = new ClientRequest({
|
|
90
198
|
method: method,
|
|
@@ -95,25 +203,55 @@ function request(url, options, callback) {
|
|
|
95
203
|
_responseCallback: callback
|
|
96
204
|
});
|
|
97
205
|
|
|
206
|
+
// RFC 9218: the request's priority, from the start (setPriority changes it).
|
|
207
|
+
if (options.priority) clientReq.setPriority(options.priority);
|
|
208
|
+
|
|
98
209
|
// Track if response was already delivered (prevent double-delivery)
|
|
99
210
|
var responded = false;
|
|
100
211
|
var _fallbackTimer = null;
|
|
101
212
|
|
|
102
|
-
// ----
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
213
|
+
// ---- Connect at once; the body follows as it is written ----
|
|
214
|
+
// It waited for end(): a body was held whole in memory, and nothing went
|
|
215
|
+
// out — not even the connection was begun — until it was complete.
|
|
216
|
+
process.nextTick(function () { doProtocolSelection(); });
|
|
217
|
+
|
|
218
|
+
// ---- The transport that carries the request ----
|
|
219
|
+
// Several may be tried at once (HTTP/3, and TCP after h3Timeout), and the
|
|
220
|
+
// first to be ready to send takes the request (claim, from each
|
|
221
|
+
// transport's onReady): the body goes to it, and to it only. One ready
|
|
222
|
+
// later is turned down and sends nothing ('superseded'). Both used to
|
|
223
|
+
// send it — the timer that starts TCP ran until the RESPONSE, so a POST
|
|
224
|
+
// whose answer took HTTP/3 more than h3Timeout went to the server twice.
|
|
225
|
+
var owner = null;
|
|
226
|
+
function attempt(name) { return { name: name, req: null }; }
|
|
227
|
+
function claim(token) {
|
|
228
|
+
if (responded || clientReq.destroyed || clientReq.aborted) return false;
|
|
229
|
+
if (owner) return owner === token;
|
|
230
|
+
owner = token;
|
|
231
|
+
// HTTP/3 has it: no TCP attempt is needed any more.
|
|
232
|
+
if (token.name === 'h3' && _fallbackTimer) { clearTimeout(_fallbackTimer); _fallbackTimer = null; }
|
|
233
|
+
if (token.req) clientReq._attachSink(token.req);
|
|
234
|
+
return true;
|
|
110
235
|
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (
|
|
116
|
-
|
|
236
|
+
// The transport's request exists (claim may have come from inside it,
|
|
237
|
+
// a pooled connection being ready at once): the body to it if it has it.
|
|
238
|
+
function bind(token, req) {
|
|
239
|
+
token.req = req;
|
|
240
|
+
if (owner === token) clientReq._attachSink(req);
|
|
241
|
+
}
|
|
242
|
+
// The attempt failed. If it carried the request, the body comes back for
|
|
243
|
+
// another — unless more of it went than was kept (ClientRequest's
|
|
244
|
+
// REPLAY_MAX): then no other can carry it, and the request fails.
|
|
245
|
+
function release(token) {
|
|
246
|
+
if (owner !== token) return true;
|
|
247
|
+
if (!clientReq._detachSink()) return false;
|
|
248
|
+
owner = null;
|
|
249
|
+
return true;
|
|
250
|
+
}
|
|
251
|
+
function failOver(token, err, again, onFail) {
|
|
252
|
+
if (!release(token)) { if (!responded) clientReq.emit('error', err); return; }
|
|
253
|
+
if (again && retryUnprocessed(err, again)) return;
|
|
254
|
+
onFail(err);
|
|
117
255
|
}
|
|
118
256
|
|
|
119
257
|
|
|
@@ -124,6 +262,26 @@ function request(url, options, callback) {
|
|
|
124
262
|
function doProtocolSelection() {
|
|
125
263
|
var cached = agent ? agent.getProtocol(hostname, port) : null;
|
|
126
264
|
|
|
265
|
+
// The origin advertised HTTP/3 (Alt-Svc): try it first, at the advertised
|
|
266
|
+
// port, whatever worked here before — that is how a client that met the
|
|
267
|
+
// origin over TCP moves to HTTP/3. Should the alternative fail, it is
|
|
268
|
+
// marked broken for a while and the request goes on over TCP.
|
|
269
|
+
var alt = agent && agent.getAltSvc ? agent.getAltSvc(hostname, port) : null;
|
|
270
|
+
if (alt && alt.port !== port) {
|
|
271
|
+
var tryAlt = function (onFail) {
|
|
272
|
+
tryH3(function (err) { agent.markAltSvcBroken(hostname, port); onFail(err); }, alt.port);
|
|
273
|
+
};
|
|
274
|
+
// Then the origin itself: over TCP as it last worked, and over HTTP/3 at
|
|
275
|
+
// its own port — an origin that answered over HTTP/3 may have sent the
|
|
276
|
+
// advertisement.
|
|
277
|
+
var rest = [];
|
|
278
|
+
if (cached === 'https' && enableH1) { rest.push(tryHttps); if (enableH2) rest.push(tryH2); }
|
|
279
|
+
else { if (enableH2) rest.push(tryH2); if (enableH1) rest.push(tryHttps); }
|
|
280
|
+
if (cached === 'h3') rest.unshift(tryH3); else rest.push(tryH3);
|
|
281
|
+
runChainWithH3Timeout([tryAlt].concat(rest), h3Timeout);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
127
285
|
// Build ordered fallback chain
|
|
128
286
|
// H3 is always first (that's the point of quico)
|
|
129
287
|
var chain = [tryH3];
|
|
@@ -146,20 +304,34 @@ function request(url, options, callback) {
|
|
|
146
304
|
/**
|
|
147
305
|
* Run through the fallback chain. If step N fails, try step N+1.
|
|
148
306
|
*/
|
|
149
|
-
function runChain(chain, index) {
|
|
307
|
+
function runChain(chain, index, lastErr) {
|
|
150
308
|
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
151
309
|
if (index >= chain.length) {
|
|
152
|
-
// All protocols failed
|
|
153
|
-
|
|
310
|
+
// All protocols failed — the last one's reason as the `cause`; it was
|
|
311
|
+
// dropped, and "all protocols failed" was all a caller could learn.
|
|
312
|
+
clientReq.emit('error', new Error('All protocols failed for ' + hostname + ':' + port, lastErr ? { cause: lastErr } : undefined));
|
|
154
313
|
return;
|
|
155
314
|
}
|
|
156
315
|
|
|
157
316
|
chain[index](function onFail(err) {
|
|
158
317
|
if (responded) return;
|
|
159
|
-
|
|
318
|
+
if (isRequestError(err)) { clientReq.emit('error', err); return; }
|
|
319
|
+
runChain(chain, index + 1, err);
|
|
160
320
|
});
|
|
161
321
|
}
|
|
162
322
|
|
|
323
|
+
// An error about the request itself — not about reaching the server over
|
|
324
|
+
// one protocol — fails the request as it is: another protocol would only
|
|
325
|
+
// send the same faulty request again, and the caller would be told "all
|
|
326
|
+
// protocols failed" instead of what is wrong.
|
|
327
|
+
// ECH rejected (after the one retry with new keys, or with none offered)
|
|
328
|
+
// ends the request with that reason: the same server, proven for its
|
|
329
|
+
// public name, would decline it over every protocol — and "all protocols
|
|
330
|
+
// failed" would hide why.
|
|
331
|
+
function isRequestError(err) {
|
|
332
|
+
return !!(err && (err.requestError || err.code === 'ERR_ECH_REJECTED'));
|
|
333
|
+
}
|
|
334
|
+
|
|
163
335
|
/**
|
|
164
336
|
* Special first-run: try H3 with a timeout.
|
|
165
337
|
* If H3 doesn't connect in time, start fallbacks in parallel.
|
|
@@ -182,12 +354,13 @@ function request(url, options, callback) {
|
|
|
182
354
|
_fallbackTimer = timer;
|
|
183
355
|
if (timer.unref) timer.unref(); // Don't prevent process exit
|
|
184
356
|
|
|
185
|
-
// Try H3
|
|
186
|
-
|
|
357
|
+
// Try H3 (the chain's first step)
|
|
358
|
+
chain[0](function onFail(err) {
|
|
187
359
|
clearTimeout(timer);
|
|
360
|
+
if (isRequestError(err) && !responded) { h3Done = true; clientReq.emit('error', err); return; }
|
|
188
361
|
if (!h3Done && !responded) {
|
|
189
362
|
h3Done = true;
|
|
190
|
-
runChain(chain.slice(1), 0);
|
|
363
|
+
runChain(chain.slice(1), 0, err);
|
|
191
364
|
}
|
|
192
365
|
});
|
|
193
366
|
}
|
|
@@ -197,13 +370,96 @@ function request(url, options, callback) {
|
|
|
197
370
|
// H3 transport (wraps existing h3_client.js)
|
|
198
371
|
// ============================================================
|
|
199
372
|
|
|
200
|
-
|
|
373
|
+
// h3Port: where to try HTTP/3 — the origin's own port unless an Alt-Svc
|
|
374
|
+
// alternative is being tried.
|
|
375
|
+
// A request the server did not process — refused (HTTP/2's
|
|
376
|
+
// REFUSED_STREAM, HTTP/3's H3_REQUEST_REJECTED) or past its GOAWAY — may be
|
|
377
|
+
// sent again (RFC 9113 §8.7, RFC 9114 §4.1.1, §5.2): once, on the same
|
|
378
|
+
// protocol, so a new connection. It fell to the next protocol in the
|
|
379
|
+
// chain, or with none left failed — a server that closes its connections
|
|
380
|
+
// after N requests (maxRequestsPerSocket) failed the requests in flight
|
|
381
|
+
// as it did. The body is back here (release), whole — or the request fails.
|
|
382
|
+
var retriedUnprocessed = false;
|
|
383
|
+
// TLS early data (HTTP/2, HTTP/1.1): what became of it — the transport's
|
|
384
|
+
// word, or 'rejected' when a 425 sent the request again after the handshake.
|
|
385
|
+
var tooEarlyRetried = false;
|
|
386
|
+
function earlyOutcome(transportReq) {
|
|
387
|
+
if (tooEarlyRetried) return 'rejected';
|
|
388
|
+
return transportReq && transportReq.earlyData ? transportReq.earlyData : null;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function retryUnprocessed(err, again) {
|
|
392
|
+
if (err && err.code === 'ERR_HTTP_TOO_EARLY') tooEarlyRetried = true;
|
|
393
|
+
// ECH rejected, with the server's current keys (lemon-tls's
|
|
394
|
+
// ERR_ECH_REJECTED): once more, with them. A rejection without keys is
|
|
395
|
+
// the server's ECH turned off — going on without ECH is the caller's
|
|
396
|
+
// decision, so it fails.
|
|
397
|
+
if (err && err.code === 'ERR_ECH_REJECTED' && echKept &&
|
|
398
|
+
!responded && !clientReq.destroyed && !clientReq.aborted) {
|
|
399
|
+
agent._evictEchRetry(echKey);
|
|
400
|
+
echKept = null;
|
|
401
|
+
echConfig = echOriginal;
|
|
402
|
+
process.nextTick(function () {
|
|
403
|
+
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
404
|
+
again();
|
|
405
|
+
});
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
if (err && err.code === 'ERR_ECH_REJECTED' && err.retryConfigs && !echRetried &&
|
|
409
|
+
!responded && !clientReq.destroyed && !clientReq.aborted) {
|
|
410
|
+
echRetried = true;
|
|
411
|
+
echConfig = err.retryConfigs;
|
|
412
|
+
process.nextTick(function () {
|
|
413
|
+
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
414
|
+
again();
|
|
415
|
+
});
|
|
416
|
+
return true;
|
|
417
|
+
}
|
|
418
|
+
if (!err || !err.retryable || retriedUnprocessed) return false;
|
|
419
|
+
if (responded || clientReq.destroyed || clientReq.aborted) return false;
|
|
420
|
+
retriedUnprocessed = true;
|
|
421
|
+
// After the event that failed it has reached every listener — the
|
|
422
|
+
// agent's among them, which takes the connection out of its pool; run
|
|
423
|
+
// inside it, the retry was handed the same GOAWAY'd connection.
|
|
424
|
+
process.nextTick(function () {
|
|
425
|
+
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
426
|
+
again();
|
|
427
|
+
});
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// res.socket over HTTP/3: there is no TLS socket — the handshake ran in
|
|
432
|
+
// the QUIC connection — so what an application asks a TLSSocket for here
|
|
433
|
+
// is answered from that connection: as over HTTP/2 and HTTP/1.1, whose
|
|
434
|
+
// res.socket is the lemon-tls socket.
|
|
435
|
+
function h3SocketInfo(conn) {
|
|
436
|
+
var q = conn && conn.quic;
|
|
437
|
+
if (!q) return null;
|
|
438
|
+
return {
|
|
439
|
+
encrypted: true,
|
|
440
|
+
alpnProtocol: 'h3',
|
|
441
|
+
get remoteAddress() { return q.remoteAddress || null; },
|
|
442
|
+
get remotePort() { return q.remotePort || null; },
|
|
443
|
+
get echStatus() { return q.echStatus; },
|
|
444
|
+
get echAccepted() { return q.echStatus === 'accepted'; },
|
|
445
|
+
isSessionReused: function () { return !!(q.isSessionReused && q.isSessionReused()); },
|
|
446
|
+
getPeerCertificate: function () { return q.getPeerCertificate ? q.getPeerCertificate() : {}; },
|
|
447
|
+
getNegotiationResult: function () { return q.getNegotiationResult ? q.getNegotiationResult() : null; },
|
|
448
|
+
// The connection's options while it runs (quic_connection's
|
|
449
|
+
// setOptions) — shared by every request on it (a pooled connection).
|
|
450
|
+
setOptions: function (o) { q.setOptions(o); return this; },
|
|
451
|
+
getOptions: function () { return q.getOptions(); }
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function tryH3(onFail, h3Port) {
|
|
201
456
|
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
457
|
+
h3Port = h3Port || port;
|
|
202
458
|
|
|
203
459
|
try {
|
|
204
460
|
var h3Opts = {
|
|
205
461
|
hostname: hostname,
|
|
206
|
-
port:
|
|
462
|
+
port: h3Port,
|
|
207
463
|
path: path,
|
|
208
464
|
method: method,
|
|
209
465
|
headers: Object.assign({}, headers),
|
|
@@ -212,26 +468,43 @@ function request(url, options, callback) {
|
|
|
212
468
|
// private-CA servers, and every such request silently fell back to
|
|
213
469
|
// TCP (or hung when the server was QUIC-only).
|
|
214
470
|
rejectUnauthorized: options.rejectUnauthorized,
|
|
215
|
-
ca: options.ca
|
|
471
|
+
ca: options.ca,
|
|
472
|
+
// The name for SNI and the certificate check, as over TCP (tls.connect's
|
|
473
|
+
// servername) — HTTP/3 used the hostname whatever was given, so a
|
|
474
|
+
// request to an address with a name of its own worked over TCP alone.
|
|
475
|
+
servername: options.servername,
|
|
476
|
+
key: options.key, cert: options.cert, passphrase: options.passphrase,
|
|
477
|
+
requestOCSP: options.requestOCSP === true,
|
|
478
|
+
ech: echConfig, echGrease: echGrease, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs,
|
|
479
|
+
startupCap: options.startupCap, pmtuDiscovery: options.pmtuDiscovery,
|
|
480
|
+
maxUdpPayloadSize: options.maxUdpPayloadSize, initialRtt: options.initialRtt,
|
|
481
|
+
reorderWindow: options.reorderWindow, initialCongestionWindow: options.initialCongestionWindow,
|
|
482
|
+
// 0-RTT with the saved session (below), off unless asked for.
|
|
483
|
+
earlyData: options.earlyData === true
|
|
216
484
|
};
|
|
485
|
+
h3Opts.quicVersions = options.quicVersions;
|
|
486
|
+
var sess = tlsSession('quic', h3Port);
|
|
487
|
+
sess.into(h3Opts);
|
|
488
|
+
// The server's NEW_TOKEN token from the last connection, used once.
|
|
489
|
+
var tokenKey = (pool && typeof pool._takeToken === 'function') ? pool._sessionKey('quic', hostname, h3Port, options) : null;
|
|
217
490
|
|
|
218
491
|
// Check Agent for existing QUIC connection (multiplexing).
|
|
219
492
|
// With an agent, the connection's lifecycle is owned by the pool
|
|
220
493
|
// (sweep / idle timer / agent.destroy), so signal h3_client not to
|
|
221
494
|
// auto-close it when this request finishes — otherwise the first
|
|
222
495
|
// request would tear down a connection still pooled for reuse.
|
|
223
|
-
if (
|
|
496
|
+
if (pool) {
|
|
224
497
|
h3Opts._managed = true;
|
|
225
|
-
var existing =
|
|
498
|
+
var existing = pool.getH3Connection(poolHost, h3Port);
|
|
226
499
|
if (existing) {
|
|
227
500
|
h3Opts._connection = existing;
|
|
228
|
-
} else if (
|
|
229
|
-
|
|
501
|
+
} else if (pool.joinH3Pending &&
|
|
502
|
+
pool.joinH3Pending(poolHost, h3Port, function () {
|
|
230
503
|
// The lead settled. Retry from the top: on success the
|
|
231
504
|
// pool lookup now hits and we multiplex on the lead's
|
|
232
505
|
// connection; on failure we become the next lead — a
|
|
233
506
|
// serial retry, never a thundering herd.
|
|
234
|
-
tryH3(onFail);
|
|
507
|
+
tryH3(onFail, h3Port);
|
|
235
508
|
})) {
|
|
236
509
|
return; // parked as a follower until the lead settles
|
|
237
510
|
}
|
|
@@ -239,14 +512,17 @@ function request(url, options, callback) {
|
|
|
239
512
|
// run on every exit path, or followers park forever.
|
|
240
513
|
}
|
|
241
514
|
|
|
242
|
-
var _pendingLead = !!(
|
|
515
|
+
var _pendingLead = !!(pool && !h3Opts._connection);
|
|
243
516
|
function _settleH3Pending(conn) {
|
|
244
517
|
if (!_pendingLead) return;
|
|
245
518
|
_pendingLead = false;
|
|
246
|
-
if (
|
|
247
|
-
else if (conn)
|
|
519
|
+
if (pool.resolveH3Pending) pool.resolveH3Pending(poolHost, h3Port, conn);
|
|
520
|
+
else if (conn) pool.setH3Connection(poolHost, h3Port, conn);
|
|
248
521
|
}
|
|
249
522
|
|
|
523
|
+
if (tokenKey && !h3Opts._connection) h3Opts.token = pool._takeToken(tokenKey);
|
|
524
|
+
var token = attempt('h3');
|
|
525
|
+
h3Opts.onReady = function () { return claim(token); };
|
|
250
526
|
var h3Req = h3Request(h3Opts, function (h3Res) {
|
|
251
527
|
if (responded) return;
|
|
252
528
|
|
|
@@ -259,27 +535,66 @@ function request(url, options, callback) {
|
|
|
259
535
|
httpVersion: '3.0',
|
|
260
536
|
statusCode: h3Res.statusCode,
|
|
261
537
|
statusMessage: '',
|
|
262
|
-
headers: h3Res.headers || {}
|
|
538
|
+
headers: h3Res.headers || {},
|
|
539
|
+
socket: h3SocketInfo(h3Req._connection || h3Opts._connection)
|
|
263
540
|
});
|
|
541
|
+
// 0-RTT: 'accepted', 'rejected' (it went again after the handshake), or null.
|
|
542
|
+
res.earlyData = h3Res.earlyData != null ? h3Res.earlyData : null;
|
|
264
543
|
|
|
265
544
|
clientReq.protocol = 'h3';
|
|
266
|
-
|
|
545
|
+
// "HTTP/3 works at the origin's own port" — not true of an alternative
|
|
546
|
+
// elsewhere, which Alt-Svc already records.
|
|
547
|
+
if (agent && h3Port === port) agent.setProtocol(hostname, port, 'h3');
|
|
548
|
+
// An HTTP/3 response may carry Alt-Svc too (a change, or `clear`).
|
|
549
|
+
if (agent && h3Res.headers && h3Res.headers['alt-svc']) {
|
|
550
|
+
agent.parseAltSvc(hostname, port, h3Res.headers['alt-svc']);
|
|
551
|
+
}
|
|
267
552
|
|
|
268
|
-
|
|
269
|
-
|
|
553
|
+
// Credit for what the application has read, not for what arrived
|
|
554
|
+
// (utils' creditAsRead), as the server does with request bodies: the
|
|
555
|
+
// server sends no faster than this response is consumed.
|
|
556
|
+
var received = null;
|
|
557
|
+
if (typeof h3Res._holdBody === 'function') {
|
|
558
|
+
h3Res._holdBody();
|
|
559
|
+
received = creditAsRead(res, function (n) { h3Res._creditBody(n); });
|
|
560
|
+
res.on('close', function () { h3Res._releaseBody(); });
|
|
561
|
+
}
|
|
562
|
+
h3Res.on('data', function (chunk) { if (received) received(chunk.length); res._pushData(chunk); });
|
|
563
|
+
h3Res.on('end', function () {
|
|
564
|
+
// Trailers arrive before the end (gRPC's grpc-status): on res by 'end'.
|
|
565
|
+
if (h3Res.trailers && Object.keys(h3Res.trailers).length) {
|
|
566
|
+
res.trailers = h3Res.trailers;
|
|
567
|
+
res.rawTrailers = h3Res.rawTrailers || [];
|
|
568
|
+
}
|
|
569
|
+
res._endData();
|
|
570
|
+
});
|
|
270
571
|
h3Res.on('error', function (err) { res._abort(err); });
|
|
271
572
|
|
|
272
573
|
deliverResponse(res);
|
|
273
574
|
});
|
|
274
575
|
|
|
576
|
+
sess.keep(h3Req);
|
|
577
|
+
if (tokenKey) h3Req.on('newToken', function (t) { pool._cacheToken(tokenKey, t); });
|
|
578
|
+
// tls.connect's requestOCSP: the server's stapled response, on this request.
|
|
579
|
+
h3Req.on('OCSPResponse', function (r) { clientReq.emit('OCSPResponse', r); });
|
|
580
|
+
// 1xx (103 Early Hints): node:http's 'information', on every version.
|
|
581
|
+
h3Req.on('information', function (info) { clientReq.emit('information', info); });
|
|
275
582
|
h3Req.on('error', function (err) {
|
|
583
|
+
if (!h3Opts._connection && !err.retryable) sess.failed();
|
|
276
584
|
_settleH3Pending(null); // release followers before failing over
|
|
277
|
-
|
|
585
|
+
failOver(token, err, function () { tryH3(onFail, h3Port); }, onFail);
|
|
586
|
+
});
|
|
587
|
+
// Another protocol had the request first: this connection is kept for
|
|
588
|
+
// the pool (and the requests parked behind it), and nothing is sent.
|
|
589
|
+
h3Req.on('superseded', function () { _settleH3Pending(h3Req._connection || null); });
|
|
590
|
+
// req.destroy(): cancel this attempt's stream. A pool lead hands its
|
|
591
|
+
// connection (if it has one yet) to the requests parked behind it.
|
|
592
|
+
clientReq._onCancel(function () {
|
|
593
|
+
_settleH3Pending(h3Req._connection || null);
|
|
594
|
+
h3Req.destroy();
|
|
278
595
|
});
|
|
279
596
|
|
|
280
|
-
|
|
281
|
-
if (body) h3Req.write(body);
|
|
282
|
-
h3Req.end();
|
|
597
|
+
bind(token, h3Req);
|
|
283
598
|
|
|
284
599
|
} catch (err) {
|
|
285
600
|
if (typeof _settleH3Pending === 'function') _settleH3Pending(null);
|
|
@@ -289,77 +604,77 @@ function request(url, options, callback) {
|
|
|
289
604
|
|
|
290
605
|
|
|
291
606
|
// ============================================================
|
|
292
|
-
// H2 transport (
|
|
607
|
+
// H2 transport (ours)
|
|
608
|
+
//
|
|
609
|
+
// Was node:http2. h2_client speaks the same shape — an emitter that emits
|
|
610
|
+
// 'response' with an IncomingMessage — so the fallback chain around it is
|
|
611
|
+
// unchanged. Sessions are pooled and coalesced per origin by the agent, the
|
|
612
|
+
// same way H3 connections are.
|
|
293
613
|
// ============================================================
|
|
294
614
|
|
|
615
|
+
// agent: false (or null) — node:https's meaning: a connection of this
|
|
616
|
+
// request's own, closed when it is done. HTTP/2 and HTTP/1.1 fell back
|
|
617
|
+
// to their modules' shared pools instead: a request with agent: false
|
|
618
|
+
// went on another's connection, kept open after it (and its `session`
|
|
619
|
+
// option, read only when a connection is made, was never used).
|
|
620
|
+
function oneOffH2() {
|
|
621
|
+
var p = new H2Pool({});
|
|
622
|
+
function done() { setImmediate(function () { p.destroy(); }); }
|
|
623
|
+
clientReq.once('response', function (res) { res.once('end', done); res.once('close', done); });
|
|
624
|
+
clientReq.once('error', done);
|
|
625
|
+
clientReq.once('close', done);
|
|
626
|
+
return p;
|
|
627
|
+
}
|
|
628
|
+
|
|
295
629
|
function tryH2(onFail) {
|
|
296
630
|
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
297
631
|
|
|
298
632
|
try {
|
|
299
|
-
var
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
var h2Req = session.request(h2Headers);
|
|
325
|
-
|
|
326
|
-
h2Req.on('response', function (responseHeaders) {
|
|
633
|
+
var sess = tlsSession('tcp:h2', port);
|
|
634
|
+
var token = attempt('h2');
|
|
635
|
+
var h2Req = h2Request(sess.into({
|
|
636
|
+
onReady: function () { return claim(token); },
|
|
637
|
+
hostname: hostname,
|
|
638
|
+
port: port,
|
|
639
|
+
path: path,
|
|
640
|
+
method: method,
|
|
641
|
+
headers: Object.assign({}, headers),
|
|
642
|
+
tls: lemonTls,
|
|
643
|
+
ca: options.ca,
|
|
644
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
645
|
+
servername: options.servername,
|
|
646
|
+
key: options.key, cert: options.cert, passphrase: options.passphrase,
|
|
647
|
+
requestOCSP: options.requestOCSP === true,
|
|
648
|
+
ech: echConfig, echGrease: echGrease, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs,
|
|
649
|
+
// One session per origin, shared with every other request through this
|
|
650
|
+
// agent — that is what multiplexing means and why the H1 pool cannot
|
|
651
|
+
// be reused here.
|
|
652
|
+
pool: pool ? agentH2Pool(pool) : oneOffH2(),
|
|
653
|
+
// 0-RTT over TLS (see h2_client); not again after a 425.
|
|
654
|
+
earlyData: options.earlyData === true && !tooEarlyRetried
|
|
655
|
+
}), function (res) {
|
|
327
656
|
if (responded) return;
|
|
328
657
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
for (var k in responseHeaders) {
|
|
332
|
-
if (k[0] !== ':') flatHeaders[k] = responseHeaders[k];
|
|
658
|
+
if (agent && res.headers['alt-svc']) {
|
|
659
|
+
agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
|
|
333
660
|
}
|
|
334
661
|
|
|
335
|
-
// Alt-Svc discovery
|
|
336
|
-
if (agent && flatHeaders['alt-svc']) {
|
|
337
|
-
agent.parseAltSvc(hostname, port, flatHeaders['alt-svc']);
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
var res = new IncomingMessage({
|
|
341
|
-
httpVersion: '2.0',
|
|
342
|
-
statusCode: statusCode,
|
|
343
|
-
headers: flatHeaders
|
|
344
|
-
});
|
|
345
|
-
|
|
346
662
|
clientReq.protocol = 'h2';
|
|
347
663
|
if (agent) agent.setProtocol(hostname, port, 'h2');
|
|
348
|
-
|
|
349
|
-
h2Req.on('data', function (chunk) { res._pushData(chunk); });
|
|
350
|
-
h2Req.on('end', function () { res._endData(); });
|
|
351
|
-
h2Req.on('error', function (err) { res._abort(err); });
|
|
352
|
-
|
|
664
|
+
res.earlyData = earlyOutcome(h2Req);
|
|
353
665
|
deliverResponse(res);
|
|
354
666
|
});
|
|
355
667
|
|
|
668
|
+
sess.keep(h2Req);
|
|
669
|
+
h2Req.on('OCSPResponse', function (r) { clientReq.emit('OCSPResponse', r); });
|
|
670
|
+
h2Req.on('information', function (info) { clientReq.emit('information', info); });
|
|
356
671
|
h2Req.on('error', function (err) {
|
|
357
|
-
|
|
672
|
+
if (!err.retryable) sess.failed();
|
|
673
|
+
failOver(token, err, function () { tryH2(onFail); }, onFail);
|
|
358
674
|
});
|
|
675
|
+
clientReq._onCancel(function () { h2Req.destroy(); }); // RST_STREAM (CANCEL)
|
|
359
676
|
|
|
360
|
-
|
|
361
|
-
if (body) h2Req.write(Buffer.from(body));
|
|
362
|
-
h2Req.end();
|
|
677
|
+
bind(token, h2Req);
|
|
363
678
|
|
|
364
679
|
} catch (err) {
|
|
365
680
|
onFail(err);
|
|
@@ -368,53 +683,56 @@ function request(url, options, callback) {
|
|
|
368
683
|
|
|
369
684
|
|
|
370
685
|
// ============================================================
|
|
371
|
-
// HTTPS transport (
|
|
686
|
+
// HTTPS transport (ours — HTTP/1.1 over lemon-tls)
|
|
372
687
|
// ============================================================
|
|
373
688
|
|
|
374
689
|
function tryHttps(onFail) {
|
|
375
690
|
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
376
691
|
|
|
377
692
|
try {
|
|
378
|
-
var
|
|
693
|
+
var sess = tlsSession('tcp:http/1.1', port);
|
|
694
|
+
var token = attempt('https');
|
|
695
|
+
var httpsReq = h1Request(sess.into({
|
|
696
|
+
onReady: function () { return claim(token); },
|
|
379
697
|
hostname: hostname,
|
|
380
698
|
port: port,
|
|
381
699
|
path: path,
|
|
382
700
|
method: method,
|
|
383
701
|
headers: Object.assign({}, headers),
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
702
|
+
tls: lemonTls,
|
|
703
|
+
ca: options.ca,
|
|
704
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
705
|
+
servername: options.servername,
|
|
706
|
+
key: options.key, cert: options.cert, passphrase: options.passphrase,
|
|
707
|
+
requestOCSP: options.requestOCSP === true,
|
|
708
|
+
ech: echConfig, echGrease: echGrease, ecdhCurve: options.ecdhCurve, sigalgs: options.sigalgs,
|
|
709
|
+
pool: pool ? agentH1Pool(pool) : new H1Pool({ keepAlive: false }),
|
|
710
|
+
earlyData: options.earlyData === true && !tooEarlyRetried
|
|
711
|
+
}), function (res) {
|
|
388
712
|
if (responded) return;
|
|
389
713
|
|
|
390
|
-
if (agent &&
|
|
391
|
-
agent.parseAltSvc(hostname, port,
|
|
714
|
+
if (agent && res.headers['alt-svc']) {
|
|
715
|
+
agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
|
|
392
716
|
}
|
|
393
717
|
|
|
394
|
-
var res = new IncomingMessage({
|
|
395
|
-
httpVersion: httpsRes.httpVersion || '1.1',
|
|
396
|
-
statusCode: httpsRes.statusCode,
|
|
397
|
-
statusMessage: httpsRes.statusMessage || '',
|
|
398
|
-
headers: httpsRes.headers || {}
|
|
399
|
-
});
|
|
400
|
-
|
|
401
718
|
clientReq.protocol = 'https';
|
|
402
719
|
if (agent) agent.setProtocol(hostname, port, 'https');
|
|
403
|
-
|
|
404
|
-
httpsRes.on('data', function (chunk) { res._pushData(chunk); });
|
|
405
|
-
httpsRes.on('end', function () { res._endData(); });
|
|
406
|
-
httpsRes.on('error', function (err) { res._abort(err); });
|
|
407
|
-
|
|
720
|
+
res.earlyData = earlyOutcome(httpsReq);
|
|
408
721
|
deliverResponse(res);
|
|
409
722
|
});
|
|
410
723
|
|
|
724
|
+
sess.keep(httpsReq);
|
|
725
|
+
httpsReq.on('OCSPResponse', function (r) { clientReq.emit('OCSPResponse', r); });
|
|
726
|
+
httpsReq.on('information', function (info) { clientReq.emit('information', info); });
|
|
727
|
+
// A retryable failure (425 Too Early to a request sent in TLS early
|
|
728
|
+
// data) goes again, as over HTTP/2; anything else falls over.
|
|
411
729
|
httpsReq.on('error', function (err) {
|
|
412
|
-
|
|
730
|
+
if (!err.retryable) sess.failed();
|
|
731
|
+
failOver(token, err, err.retryable ? function () { tryHttps(onFail); } : null, onFail);
|
|
413
732
|
});
|
|
733
|
+
clientReq._onCancel(function () { httpsReq.destroy(); }); // the socket goes
|
|
414
734
|
|
|
415
|
-
|
|
416
|
-
if (body) httpsReq.write(Buffer.from(body));
|
|
417
|
-
httpsReq.end();
|
|
735
|
+
bind(token, httpsReq);
|
|
418
736
|
|
|
419
737
|
} catch (err) {
|
|
420
738
|
onFail(err);
|
|
@@ -429,6 +747,15 @@ function request(url, options, callback) {
|
|
|
429
747
|
function deliverResponse(res) {
|
|
430
748
|
if (responded) return;
|
|
431
749
|
responded = true;
|
|
750
|
+
// The server's new ECH keys worked: kept for the next requests.
|
|
751
|
+
if (echRetried && echKey) agent._cacheEchRetry(echKey, echConfig);
|
|
752
|
+
clientReq.res = res;
|
|
753
|
+
// The application destroying the response mid-way gives the request up
|
|
754
|
+
// with it — the stream cancelled (STOP_SENDING and RESET_STREAM over
|
|
755
|
+
// HTTP/3, RST_STREAM over HTTP/2), the socket over HTTP/1.1 — as
|
|
756
|
+
// node:http's res.destroy() destroys the socket. The rest of the body
|
|
757
|
+
// kept coming, at full speed, into a response nobody read.
|
|
758
|
+
if (typeof res._onDestroy === 'function') res._onDestroy(function () { if (!res.complete && !clientReq.destroyed) clientReq.destroy(); });
|
|
432
759
|
|
|
433
760
|
if (clientReq._timeoutTimer) {
|
|
434
761
|
clearTimeout(clientReq._timeoutTimer);
|