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/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,140 +370,311 @@ 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
|
-
headers: Object.assign({}, headers)
|
|
465
|
+
headers: Object.assign({}, headers),
|
|
466
|
+
// The H2 and HTTPS fallbacks below honour this; dropping it here made
|
|
467
|
+
// H3 — alone of the three — unable to talk to self-signed or
|
|
468
|
+
// private-CA servers, and every such request silently fell back to
|
|
469
|
+
// TCP (or hung when the server was QUIC-only).
|
|
470
|
+
rejectUnauthorized: options.rejectUnauthorized,
|
|
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
|
|
210
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;
|
|
211
490
|
|
|
212
491
|
// Check Agent for existing QUIC connection (multiplexing).
|
|
213
492
|
// With an agent, the connection's lifecycle is owned by the pool
|
|
214
493
|
// (sweep / idle timer / agent.destroy), so signal h3_client not to
|
|
215
494
|
// auto-close it when this request finishes — otherwise the first
|
|
216
495
|
// request would tear down a connection still pooled for reuse.
|
|
217
|
-
if (
|
|
496
|
+
if (pool) {
|
|
218
497
|
h3Opts._managed = true;
|
|
219
|
-
var existing =
|
|
220
|
-
if (existing)
|
|
498
|
+
var existing = pool.getH3Connection(poolHost, h3Port);
|
|
499
|
+
if (existing) {
|
|
500
|
+
h3Opts._connection = existing;
|
|
501
|
+
} else if (pool.joinH3Pending &&
|
|
502
|
+
pool.joinH3Pending(poolHost, h3Port, function () {
|
|
503
|
+
// The lead settled. Retry from the top: on success the
|
|
504
|
+
// pool lookup now hits and we multiplex on the lead's
|
|
505
|
+
// connection; on failure we become the next lead — a
|
|
506
|
+
// serial retry, never a thundering herd.
|
|
507
|
+
tryH3(onFail, h3Port);
|
|
508
|
+
})) {
|
|
509
|
+
return; // parked as a follower until the lead settles
|
|
510
|
+
}
|
|
511
|
+
// else: we are the LEAD for this origin. _settleH3Pending below MUST
|
|
512
|
+
// run on every exit path, or followers park forever.
|
|
221
513
|
}
|
|
222
514
|
|
|
515
|
+
var _pendingLead = !!(pool && !h3Opts._connection);
|
|
516
|
+
function _settleH3Pending(conn) {
|
|
517
|
+
if (!_pendingLead) return;
|
|
518
|
+
_pendingLead = false;
|
|
519
|
+
if (pool.resolveH3Pending) pool.resolveH3Pending(poolHost, h3Port, conn);
|
|
520
|
+
else if (conn) pool.setH3Connection(poolHost, h3Port, conn);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (tokenKey && !h3Opts._connection) h3Opts.token = pool._takeToken(tokenKey);
|
|
524
|
+
var token = attempt('h3');
|
|
525
|
+
h3Opts.onReady = function () { return claim(token); };
|
|
223
526
|
var h3Req = h3Request(h3Opts, function (h3Res) {
|
|
224
527
|
if (responded) return;
|
|
225
528
|
|
|
226
|
-
//
|
|
227
|
-
if (
|
|
228
|
-
|
|
529
|
+
// Register the connection and wake coalesced followers.
|
|
530
|
+
if (h3Req._connection && !h3Opts._connection) {
|
|
531
|
+
_settleH3Pending(h3Req._connection);
|
|
229
532
|
}
|
|
230
533
|
|
|
231
534
|
var res = new IncomingMessage({
|
|
232
535
|
httpVersion: '3.0',
|
|
233
536
|
statusCode: h3Res.statusCode,
|
|
234
537
|
statusMessage: '',
|
|
235
|
-
headers: h3Res.headers || {}
|
|
538
|
+
headers: h3Res.headers || {},
|
|
539
|
+
socket: h3SocketInfo(h3Req._connection || h3Opts._connection)
|
|
236
540
|
});
|
|
541
|
+
// 0-RTT: 'accepted', 'rejected' (it went again after the handshake), or null.
|
|
542
|
+
res.earlyData = h3Res.earlyData != null ? h3Res.earlyData : null;
|
|
237
543
|
|
|
238
544
|
clientReq.protocol = 'h3';
|
|
239
|
-
|
|
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
|
+
}
|
|
240
552
|
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
});
|
|
243
571
|
h3Res.on('error', function (err) { res._abort(err); });
|
|
244
572
|
|
|
245
573
|
deliverResponse(res);
|
|
246
574
|
});
|
|
247
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); });
|
|
248
582
|
h3Req.on('error', function (err) {
|
|
249
|
-
|
|
583
|
+
if (!h3Opts._connection && !err.retryable) sess.failed();
|
|
584
|
+
_settleH3Pending(null); // release followers before failing over
|
|
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();
|
|
250
595
|
});
|
|
251
596
|
|
|
252
|
-
|
|
253
|
-
if (body) h3Req.write(body);
|
|
254
|
-
h3Req.end();
|
|
597
|
+
bind(token, h3Req);
|
|
255
598
|
|
|
256
599
|
} catch (err) {
|
|
600
|
+
if (typeof _settleH3Pending === 'function') _settleH3Pending(null);
|
|
257
601
|
onFail(err);
|
|
258
602
|
}
|
|
259
603
|
}
|
|
260
604
|
|
|
261
605
|
|
|
262
606
|
// ============================================================
|
|
263
|
-
// 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.
|
|
264
613
|
// ============================================================
|
|
265
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
|
+
|
|
266
629
|
function tryH2(onFail) {
|
|
267
630
|
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
268
631
|
|
|
269
632
|
try {
|
|
270
|
-
var
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
var h2Req = session.request(h2Headers);
|
|
296
|
-
|
|
297
|
-
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) {
|
|
298
656
|
if (responded) return;
|
|
299
657
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
for (var k in responseHeaders) {
|
|
303
|
-
if (k[0] !== ':') flatHeaders[k] = responseHeaders[k];
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Alt-Svc discovery
|
|
307
|
-
if (agent && flatHeaders['alt-svc']) {
|
|
308
|
-
agent.parseAltSvc(hostname, port, flatHeaders['alt-svc']);
|
|
658
|
+
if (agent && res.headers['alt-svc']) {
|
|
659
|
+
agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
|
|
309
660
|
}
|
|
310
661
|
|
|
311
|
-
var res = new IncomingMessage({
|
|
312
|
-
httpVersion: '2.0',
|
|
313
|
-
statusCode: statusCode,
|
|
314
|
-
headers: flatHeaders
|
|
315
|
-
});
|
|
316
|
-
|
|
317
662
|
clientReq.protocol = 'h2';
|
|
318
663
|
if (agent) agent.setProtocol(hostname, port, 'h2');
|
|
319
|
-
|
|
320
|
-
h2Req.on('data', function (chunk) { res._pushData(chunk); });
|
|
321
|
-
h2Req.on('end', function () { res._endData(); });
|
|
322
|
-
h2Req.on('error', function (err) { res._abort(err); });
|
|
323
|
-
|
|
664
|
+
res.earlyData = earlyOutcome(h2Req);
|
|
324
665
|
deliverResponse(res);
|
|
325
666
|
});
|
|
326
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); });
|
|
327
671
|
h2Req.on('error', function (err) {
|
|
328
|
-
|
|
672
|
+
if (!err.retryable) sess.failed();
|
|
673
|
+
failOver(token, err, function () { tryH2(onFail); }, onFail);
|
|
329
674
|
});
|
|
675
|
+
clientReq._onCancel(function () { h2Req.destroy(); }); // RST_STREAM (CANCEL)
|
|
330
676
|
|
|
331
|
-
|
|
332
|
-
if (body) h2Req.write(Buffer.from(body));
|
|
333
|
-
h2Req.end();
|
|
677
|
+
bind(token, h2Req);
|
|
334
678
|
|
|
335
679
|
} catch (err) {
|
|
336
680
|
onFail(err);
|
|
@@ -339,53 +683,56 @@ function request(url, options, callback) {
|
|
|
339
683
|
|
|
340
684
|
|
|
341
685
|
// ============================================================
|
|
342
|
-
// HTTPS transport (
|
|
686
|
+
// HTTPS transport (ours — HTTP/1.1 over lemon-tls)
|
|
343
687
|
// ============================================================
|
|
344
688
|
|
|
345
689
|
function tryHttps(onFail) {
|
|
346
690
|
if (responded || clientReq.destroyed || clientReq.aborted) return;
|
|
347
691
|
|
|
348
692
|
try {
|
|
349
|
-
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); },
|
|
350
697
|
hostname: hostname,
|
|
351
698
|
port: port,
|
|
352
699
|
path: path,
|
|
353
700
|
method: method,
|
|
354
701
|
headers: Object.assign({}, headers),
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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) {
|
|
359
712
|
if (responded) return;
|
|
360
713
|
|
|
361
|
-
if (agent &&
|
|
362
|
-
agent.parseAltSvc(hostname, port,
|
|
714
|
+
if (agent && res.headers['alt-svc']) {
|
|
715
|
+
agent.parseAltSvc(hostname, port, res.headers['alt-svc']);
|
|
363
716
|
}
|
|
364
717
|
|
|
365
|
-
var res = new IncomingMessage({
|
|
366
|
-
httpVersion: httpsRes.httpVersion || '1.1',
|
|
367
|
-
statusCode: httpsRes.statusCode,
|
|
368
|
-
statusMessage: httpsRes.statusMessage || '',
|
|
369
|
-
headers: httpsRes.headers || {}
|
|
370
|
-
});
|
|
371
|
-
|
|
372
718
|
clientReq.protocol = 'https';
|
|
373
719
|
if (agent) agent.setProtocol(hostname, port, 'https');
|
|
374
|
-
|
|
375
|
-
httpsRes.on('data', function (chunk) { res._pushData(chunk); });
|
|
376
|
-
httpsRes.on('end', function () { res._endData(); });
|
|
377
|
-
httpsRes.on('error', function (err) { res._abort(err); });
|
|
378
|
-
|
|
720
|
+
res.earlyData = earlyOutcome(httpsReq);
|
|
379
721
|
deliverResponse(res);
|
|
380
722
|
});
|
|
381
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.
|
|
382
729
|
httpsReq.on('error', function (err) {
|
|
383
|
-
|
|
730
|
+
if (!err.retryable) sess.failed();
|
|
731
|
+
failOver(token, err, err.retryable ? function () { tryHttps(onFail); } : null, onFail);
|
|
384
732
|
});
|
|
733
|
+
clientReq._onCancel(function () { httpsReq.destroy(); }); // the socket goes
|
|
385
734
|
|
|
386
|
-
|
|
387
|
-
if (body) httpsReq.write(Buffer.from(body));
|
|
388
|
-
httpsReq.end();
|
|
735
|
+
bind(token, httpsReq);
|
|
389
736
|
|
|
390
737
|
} catch (err) {
|
|
391
738
|
onFail(err);
|
|
@@ -400,6 +747,15 @@ function request(url, options, callback) {
|
|
|
400
747
|
function deliverResponse(res) {
|
|
401
748
|
if (responded) return;
|
|
402
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(); });
|
|
403
759
|
|
|
404
760
|
if (clientReq._timeoutTimer) {
|
|
405
761
|
clearTimeout(clientReq._timeoutTimer);
|