quico 0.4.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1987 -160
- package/index.d.ts +1034 -77
- package/index.js +59 -36
- package/package.json +8 -5
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +779 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +666 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +911 -0
- package/src/h3.js +1986 -520
- package/src/h3_client.js +484 -46
- package/src/h3_server.js +1448 -98
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +559 -0
- package/src/http_common.js +908 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6702 -665
- package/src/quic_server.js +858 -82
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +1097 -125
- package/src/server_stats.js +93 -0
- package/src/streams.js +542 -43
- package/src/tcp_listener.js +568 -0
- package/src/tls_bridge.js +478 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +784 -417
- package/src/ws.js +744 -0
- package/src/ws_client.js +621 -0
- package/src/ws_server.js +815 -0
- package/src/ws_stream.js +250 -0
- package/src/wt_stream.js +1075 -0
package/src/h3_server.js
CHANGED
|
@@ -12,10 +12,25 @@
|
|
|
12
12
|
* https://github.com/colocohen/quico
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { Duplex
|
|
16
|
-
import {
|
|
15
|
+
import { Duplex } from 'node:stream';
|
|
16
|
+
import { X509Certificate } from 'node:crypto';
|
|
17
|
+
import { readVarInt, writeVarInt, wt_datagram_payload } from './utils.js';
|
|
18
|
+
import { observeH3 } from './h2_fingerprint.js';
|
|
19
|
+
// The Emitter from http_common, not the one in utils.js. Two differences that
|
|
20
|
+
// matter here: it has listenerCount (needed to tell whether anyone wants an
|
|
21
|
+
// 'upgrade'), and it does NOT swallow exceptions thrown by listeners — the
|
|
22
|
+
// utils.js one wraps every call in a try/catch and discards what it catches,
|
|
23
|
+
// which is how a handler that throws becomes silence.
|
|
24
|
+
import { Emitter, isConnectionSpecific, runRequestHandler, validateHeaderName, validateHeaderValue, validateStatus,
|
|
25
|
+
DEFAULT_HEADERS_TIMEOUT, DEFAULT_REQUEST_TIMEOUT, startDeadline, responsePriority,
|
|
26
|
+
sfString, parseSfStringList, maxRequestsPerSocket, trailerFields, statusHasNoBody, runEarlyDataFilter, httpDate, streamResetLimiter } from './http_common.js';
|
|
17
27
|
import { createQuicServer } from './quic_server.js';
|
|
18
|
-
import {
|
|
28
|
+
import { pickTlsOptions } from './tls_options.js';
|
|
29
|
+
import { _makeSocketStub } from './streams.js';
|
|
30
|
+
import { H3Connection, H3_ERR, wtCloseCapsule, wtDrainCapsule, createCapsuleReader, h3SettingsContext } from './h3.js';
|
|
31
|
+
import { createWtStream, openWtStream, createSendGroup, wtScheduling, createDatagrams, sessionStats,
|
|
32
|
+
exporterArgs, exportSessionKeyingMaterial } from './wt_stream.js';
|
|
33
|
+
import { contextZero, datagramCapsule, STREAM_QUEUE_MAX } from './connect_udp.js';
|
|
19
34
|
|
|
20
35
|
|
|
21
36
|
// ============================================================
|
|
@@ -27,25 +42,129 @@ function createServer(options, handler) {
|
|
|
27
42
|
|
|
28
43
|
var ev = Emitter();
|
|
29
44
|
|
|
45
|
+
// node:http's shouldUpgradeCallback, for extended CONNECT (RFC 9220): taken
|
|
46
|
+
// up as an 'upgrade' (true) or handled as an ordinary request (false).
|
|
47
|
+
// Default: true when something listens for 'upgrade' — counted here, since
|
|
48
|
+
// this Emitter has no listenerCount (so the old "nobody listens" check
|
|
49
|
+
// could never fire).
|
|
50
|
+
var upgradeListeners = 0;
|
|
51
|
+
var shouldUpgradeCallback = typeof options.shouldUpgradeCallback === 'function'
|
|
52
|
+
? options.shouldUpgradeCallback
|
|
53
|
+
: function () { return upgradeListeners > 0; };
|
|
54
|
+
|
|
55
|
+
// CONNECT-UDP (RFC 9298), from server.js: { wants(req), handle(req,
|
|
56
|
+
// carrier) } — whether a request is to be proxied, and the proxy
|
|
57
|
+
// (connect_udp.js) that takes it with the carrier built here.
|
|
58
|
+
var connectUdp = options.connectUdpHandler || null;
|
|
59
|
+
|
|
30
60
|
// Transport-only QUIC server owns the UDP socket(s), demux, connection
|
|
31
61
|
// map and lifecycle. h3_server layers HTTP/3 on top by listening for its
|
|
32
62
|
// 'connection' event (see below) and wrapping each raw QUICConnection in
|
|
33
63
|
// an H3Connection. ALPN is 'h3'. External-socket (shared-port) mode and
|
|
34
64
|
// the handlePacket/hasConnection demux hooks are provided by the
|
|
35
65
|
// transport server and re-exported on this server's API.
|
|
36
|
-
|
|
37
|
-
|
|
66
|
+
// 'h3' plus whatever else the application asked the handshake to accept.
|
|
67
|
+
// Anything but 'h3' is closed once the handshake settles on it — see the
|
|
68
|
+
// 'connect' handler below.
|
|
69
|
+
var alpn = options.alpn || ['h3'];
|
|
70
|
+
|
|
71
|
+
// Datagrams an application sends between accepting a WebTransport session
|
|
72
|
+
// and its 200 leaving, kept — the oldest dropped past this many.
|
|
73
|
+
var WT_DATAGRAMS_HELD = 256;
|
|
74
|
+
|
|
75
|
+
// ---- How long a request may take to arrive (see http_common) ----
|
|
76
|
+
//
|
|
77
|
+
// headersTimeout: from a request stream's first byte to its whole HEADERS
|
|
78
|
+
// frame. Its streams are independent, so a slow head costs only itself:
|
|
79
|
+
// reset, H3_REQUEST_INCOMPLETE, before any request exists. It was not
|
|
80
|
+
// timed at all — a HEADERS frame sent a byte at a time held its stream, and
|
|
81
|
+
// its share of the connection's stream limit, for as long as it went on.
|
|
82
|
+
// requestTimeout: from the head to the request's last byte; past it the
|
|
83
|
+
// stream is reset both ways and the request ends as a cancelled one does.
|
|
84
|
+
// Not for CONNECT (a WebSocket or a WebTransport session): its stream is
|
|
85
|
+
// meant to stay open.
|
|
86
|
+
var headersTimeout = options.headersTimeout != null ? options.headersTimeout : DEFAULT_HEADERS_TIMEOUT;
|
|
87
|
+
// Rapid Reset (http_common streamResetLimiter): the options checked now.
|
|
88
|
+
streamResetLimiter(options);
|
|
89
|
+
// 0-RTT requests: the application's judge (earlyGate), or the default.
|
|
90
|
+
if (options.earlyDataFilter != null && typeof options.earlyDataFilter !== 'function') {
|
|
91
|
+
var efe = new TypeError('earlyDataFilter must be a function (req, callback)'); efe.code = 'ERR_INVALID_ARG_TYPE'; throw efe;
|
|
92
|
+
}
|
|
93
|
+
var earlyDataFilter = options.earlyDataFilter || null;
|
|
94
|
+
// node:http's maxRequestsPerSocket: GOAWAY after the Nth request (0: none).
|
|
95
|
+
var maxRequests = maxRequestsPerSocket(options.maxRequestsPerSocket);
|
|
96
|
+
var requestTimeout = options.requestTimeout != null ? options.requestTimeout : DEFAULT_REQUEST_TIMEOUT;
|
|
97
|
+
|
|
98
|
+
function armHeadersDeadline(conn, streamId) {
|
|
99
|
+
var t = startDeadline(headersTimeout, function () {
|
|
100
|
+
if (conn.headerDeadlines[streamId] !== t) return;
|
|
101
|
+
delete conn.headerDeadlines[streamId];
|
|
102
|
+
// H3_REQUEST_REJECTED: nothing of the request was processed — no
|
|
103
|
+
// headers to process — so the client may send it again (RFC 9114
|
|
104
|
+
// §4.1.1), as it may not after H3_REQUEST_INCOMPLETE.
|
|
105
|
+
if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_REQUEST_REJECTED);
|
|
106
|
+
});
|
|
107
|
+
if (t) conn.headerDeadlines[streamId] = t;
|
|
108
|
+
}
|
|
109
|
+
function clearHeadersDeadline(conn, streamId) {
|
|
110
|
+
var t = conn.headerDeadlines[streamId];
|
|
111
|
+
if (t) { clearTimeout(t); delete conn.headerDeadlines[streamId]; }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function armRequestDeadline(conn, streamId, stream) {
|
|
115
|
+
stream.deadline = startDeadline(requestTimeout, function () {
|
|
116
|
+
stream.deadline = null;
|
|
117
|
+
if (conn.http_streams[streamId] !== stream || stream.req._ended) return;
|
|
118
|
+
if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_REQUEST_INCOMPLETE);
|
|
119
|
+
releaseHttpStream(conn, streamId, true);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function clearRequestDeadline(stream) {
|
|
123
|
+
if (stream && stream.deadline) { clearTimeout(stream.deadline); stream.deadline = null; }
|
|
124
|
+
}
|
|
125
|
+
if (!Array.isArray(alpn)) alpn = [alpn];
|
|
126
|
+
if (alpn.indexOf('h3') === -1) alpn = ['h3'].concat(alpn);
|
|
127
|
+
|
|
128
|
+
var qserver = createQuicServer(Object.assign({
|
|
129
|
+
alpn: alpn,
|
|
38
130
|
SNICallback: options.SNICallback || null,
|
|
131
|
+
ticketKeys: options.ticketKeys || null,
|
|
132
|
+
ticketData: options.ticketData || null,
|
|
133
|
+
ech: options.ech || null,
|
|
134
|
+
ocspRequest: options.ocspRequest || null,
|
|
135
|
+
resetTokenSecret: options.resetTokenSecret || null,
|
|
136
|
+
tokenSecret: options.tokenSecret || null,
|
|
137
|
+
validateAddress: options.validateAddress === true,
|
|
138
|
+
handshakeLoadThreshold: options.handshakeLoadThreshold,
|
|
139
|
+
earlyData: options.earlyData, earlyDataMaxAge: options.earlyDataMaxAge, earlyDataReplay: options.earlyDataReplay,
|
|
140
|
+
// 0-RTT is taken only under the SETTINGS this server sends now: a client
|
|
141
|
+
// may send early data under the ones it remembered (RFC 9114 §7.2.4.2).
|
|
142
|
+
earlyDataAppContext: h3SettingsContext({ enableWebTransport: true, maxWebTransportSessions: options.maxWebTransportSessions }),
|
|
143
|
+
maxHandshakesPerAddress: options.maxHandshakesPerAddress,
|
|
39
144
|
key: options.key,
|
|
40
145
|
cert: options.cert,
|
|
41
146
|
ca: options.ca,
|
|
42
147
|
maxConnections: options.maxConnections || 10000,
|
|
148
|
+
transportParams: options.transportParams || null,
|
|
149
|
+
initialCongestionWindow: options.initialCongestionWindow,
|
|
150
|
+
carefulResume: options.carefulResume,
|
|
151
|
+
startupCap: options.startupCap,
|
|
152
|
+
pmtuDiscovery: options.pmtuDiscovery,
|
|
153
|
+
maxUdpPayloadSize: options.maxUdpPayloadSize,
|
|
154
|
+
initialRtt: options.initialRtt,
|
|
155
|
+
reorderWindow: options.reorderWindow,
|
|
156
|
+
connectionOptions: options.connectionOptions,
|
|
157
|
+
quicVersions: options.quicVersions,
|
|
43
158
|
socket: options.socket || null,
|
|
44
|
-
socket6: options.socket6 || null
|
|
45
|
-
|
|
159
|
+
socket6: options.socket6 || null,
|
|
160
|
+
greaseQuicBit: options.greaseQuicBit,
|
|
161
|
+
// The server's counters (server_stats.js), when it keeps them.
|
|
162
|
+
stats: options.stats || null
|
|
163
|
+
}, pickTlsOptions(options))); // node:tls's options (tls_options.js)
|
|
46
164
|
|
|
47
165
|
// Surface transport-level (UDP socket / send) errors on this server.
|
|
48
166
|
qserver.on('error', function (err) { ev.emit('error', err); });
|
|
167
|
+
qserver.on('handshakeLoad', function (e) { ev.emit('handshakeLoad', e); });
|
|
49
168
|
|
|
50
169
|
|
|
51
170
|
// ============================================================
|
|
@@ -65,40 +184,357 @@ function createServer(options, handler) {
|
|
|
65
184
|
}
|
|
66
185
|
}
|
|
67
186
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
187
|
+
// ── Stream lifecycle: one exit ────────────────────────────────────────
|
|
188
|
+
//
|
|
189
|
+
// releaseHttpStream is the ONLY place this server drops what it holds for
|
|
190
|
+
// a stream — an entry in conn.http_streams (a request, a tunnel, a
|
|
191
|
+
// WebTransport session) or in conn._wtStreamObjects (a WebTransport
|
|
192
|
+
// stream) — and every way a stream can end comes through it:
|
|
193
|
+
//
|
|
194
|
+
// normally (aborted = false)
|
|
195
|
+
// a request over in both directions → releaseIfDone
|
|
196
|
+
// a tunnel's FIN, or a refused tunnel → tunnel_data / reject / 501
|
|
197
|
+
// a WebTransport stream's FIN → wt_stream / wt_data
|
|
198
|
+
// our own WT stream finished → its 'close'
|
|
199
|
+
// a session's FIN (CONNECT stream) → request_end
|
|
200
|
+
// cut short (aborted = true)
|
|
201
|
+
// the peer cancelled (RESET / STOP_SENDING) → reportCancel
|
|
202
|
+
// we ended it over what the peer sent → 'stream_error'
|
|
203
|
+
// its WebTransport session ended → releaseSessionStreams
|
|
204
|
+
// the connection closed → quic 'close'
|
|
205
|
+
//
|
|
206
|
+
// What each kind hears is decided here, once:
|
|
207
|
+
// a request cut short → 'aborted' — the counterpart of h2_server's
|
|
208
|
+
// abortStream; the unified server turns it into
|
|
209
|
+
// IncomingMessage 'aborted' and ServerResponse
|
|
210
|
+
// 'close'. Never an Error: a browser cancelling
|
|
211
|
+
// a request is ordinary traffic, and an uncaught
|
|
212
|
+
// error for it would take the process down.
|
|
213
|
+
// a tunnel cut short → its Duplex destroyed
|
|
214
|
+
// a WT stream cut short → destroyed
|
|
215
|
+
// a WT session, however → 'close' once (and 'aborted' if cut short), and
|
|
216
|
+
// every stream still open in it released too
|
|
217
|
+
// Idempotent: a second end for the same stream finds nothing.
|
|
218
|
+
function releaseHttpStream(conn, streamId, aborted) {
|
|
219
|
+
var wtStream = conn._wtStreamObjects[streamId];
|
|
220
|
+
if (wtStream) {
|
|
221
|
+
delete conn._wtStreamObjects[streamId];
|
|
222
|
+
if (aborted) { try { wtStream.destroy(); } catch (e) {} }
|
|
87
223
|
}
|
|
88
224
|
|
|
89
|
-
stream
|
|
90
|
-
stream
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
225
|
+
var stream = conn.http_streams[streamId];
|
|
226
|
+
if (!stream) return;
|
|
227
|
+
delete conn.http_streams[streamId];
|
|
228
|
+
clearRequestDeadline(stream);
|
|
229
|
+
var req = stream.req;
|
|
230
|
+
// Its body's credit, if the layer above was returning it as it read: the
|
|
231
|
+
// request is over, so nothing more is waited for.
|
|
232
|
+
if (conn.h3) conn.h3.releaseBody(streamId);
|
|
233
|
+
|
|
234
|
+
if (stream.tunnel) {
|
|
235
|
+
if (aborted) { try { stream.tunnel.destroy(); } catch (e) {} }
|
|
236
|
+
if (conn.goaway) closeAfterGoaway(conn);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if (stream.udp) {
|
|
240
|
+
var quarter = Math.floor(Number(streamId) / 4);
|
|
241
|
+
if (conn.udpByQuarter[quarter] === Number(streamId)) delete conn.udpByQuarter[quarter];
|
|
242
|
+
// Cut short under the tunnel (a reset, the connection gone): it closes.
|
|
243
|
+
if (aborted && stream.udp.sink) stream.udp.sink.abort();
|
|
244
|
+
if (conn.goaway) closeAfterGoaway(conn);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (aborted && !req.aborted) {
|
|
248
|
+
req.aborted = true;
|
|
249
|
+
var listeners = req._listeners['aborted'] || [];
|
|
250
|
+
for (var i = 0; i < listeners.length; i++) {
|
|
251
|
+
try { listeners[i](); } catch (e) {}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
if (req._isWebTransport) {
|
|
255
|
+
if (conn.wtByQuarter[Math.floor(Number(streamId) / 4)] === Number(streamId)) delete conn.wtByQuarter[Math.floor(Number(streamId) / 4)];
|
|
256
|
+
// FIN on the CONNECT stream ends the WebTransport session — it is what
|
|
257
|
+
// a browser's wt.close() sends — as does a reset of it, or the
|
|
258
|
+
// connection going. 'close' used to come only with the whole QUIC
|
|
259
|
+
// connection, and a browser that closed the session but kept the
|
|
260
|
+
// connection for its fetches left the session standing here.
|
|
261
|
+
if (!req._wtClosed) {
|
|
262
|
+
req._wtClosed = true;
|
|
263
|
+
if (req._wtDatagrams) req._wtDatagrams._close();
|
|
264
|
+
_emitOnReq(req, 'close');
|
|
265
|
+
}
|
|
266
|
+
releaseSessionStreams(conn, Number(streamId));
|
|
267
|
+
}
|
|
268
|
+
if (conn.goaway) closeAfterGoaway(conn);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// ---- Graceful shutdown (RFC 9114 §5.2) ----
|
|
272
|
+
//
|
|
273
|
+
// The intents shared with h2_server's handle (CONN_OVERLOADED is
|
|
274
|
+
// 'overloaded'), as HTTP/3 connection error codes: one piece of limiting
|
|
275
|
+
// code drives both versions.
|
|
276
|
+
function h3ConnCode(code) {
|
|
277
|
+
if (typeof code === 'number') return code;
|
|
278
|
+
if (code === 'overloaded') return H3_ERR.H3_EXCESSIVE_LOAD;
|
|
279
|
+
return H3_ERR.H3_NO_ERROR;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// After GOAWAY, once the last request accepted has finished and everything
|
|
283
|
+
// sent on the connection has been acknowledged, the connection is closed
|
|
284
|
+
// ("MAY initiate an immediate closure", §5.2). Not at the last res.end():
|
|
285
|
+
// CONNECTION_CLOSE stops all sending, and the last responses would be cut
|
|
286
|
+
// off in flight.
|
|
287
|
+
function closeAfterGoaway(conn) {
|
|
288
|
+
if (conn.goaway.closing || Object.keys(conn.http_streams).length > 0) return;
|
|
289
|
+
conn.goaway.closing = true;
|
|
290
|
+
conn.quic.whenAllSent(function () {
|
|
291
|
+
try { conn.quic.close(conn.goaway.code, 'goaway', true); } catch (e) {}
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Every WebTransport stream still open in a session that has ended.
|
|
296
|
+
// Each goes with WT_SESSION_GONE (draft-ietf-webtrans-http3 §4.6) — not
|
|
297
|
+
// with application code 0, which it got when its session's end reached it
|
|
298
|
+
// here before h3's endWtSession did (the peer's STOP_SENDING on the CONNECT
|
|
299
|
+
// stream: released first, then stopped).
|
|
300
|
+
function releaseSessionStreams(conn, sessionId) {
|
|
301
|
+
for (var k in conn._wtStreamObjects) {
|
|
302
|
+
var child = conn._wtStreamObjects[k];
|
|
303
|
+
if (child && child._sessionId === sessionId) {
|
|
304
|
+
if (child._sessionGone) child._sessionGone();
|
|
305
|
+
releaseHttpStream(conn, k, true);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
96
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Release a request's entry once it is over in both directions: the
|
|
312
|
+
* request fully received (FIN, 'request_end') and the response ended
|
|
313
|
+
* (res.end).
|
|
314
|
+
*
|
|
315
|
+
* Nothing removed it before, except a cancel, a tunnel's end, or a 501 —
|
|
316
|
+
* so every request served stayed here, with the IncomingMessage and
|
|
317
|
+
* ServerResponse wrapped around it, for as long as the QUIC connection
|
|
318
|
+
* lived. A browser keeps that connection open while a WebTransport session
|
|
319
|
+
* runs on it, and sends every fetch for the origin over it too: the map grew
|
|
320
|
+
* without bound, and the datagram handler below walks it for every datagram.
|
|
321
|
+
*
|
|
322
|
+
* A tunnel and a WebTransport session have their own ends (tunnel_data FIN,
|
|
323
|
+
* 'request_end' on the CONNECT stream) and are left to them.
|
|
324
|
+
*/
|
|
325
|
+
function releaseIfDone(conn, streamId) {
|
|
326
|
+
var stream = conn.http_streams[streamId];
|
|
327
|
+
if (!stream || stream.tunnel || stream.udp || stream.req._isWebTransport) return;
|
|
328
|
+
if (stream.req._ended && stream.res._finished) releaseHttpStream(conn, streamId, false);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// A WebTransport stream (wt_stream.js — the client builds its own the same
|
|
332
|
+
// way), recorded on its connection so a session's end can find it, and
|
|
333
|
+
// forgotten when it closes.
|
|
334
|
+
// prefixLength: on a stream we opened, the prefix h3 wrote on it.
|
|
335
|
+
function _createWtStream(conn, quic, wtStreamId, sessionId, kind, prefixLength) {
|
|
336
|
+
var session = conn.http_streams[sessionId];
|
|
337
|
+
var stream = createWtStream(quic, wtStreamId, kind, function () {
|
|
338
|
+
if (conn._wtStreamObjects[wtStreamId] === stream) delete conn._wtStreamObjects[wtStreamId];
|
|
339
|
+
if (conn.h3) conn.h3.releaseWebTransportStream(wtStreamId);
|
|
340
|
+
}, prefixLength, session && session.req._wtSched, sessionId);
|
|
97
341
|
conn._wtStreamObjects[wtStreamId] = stream;
|
|
98
342
|
return stream;
|
|
99
343
|
}
|
|
100
344
|
|
|
101
345
|
|
|
346
|
+
/**
|
|
347
|
+
* A Duplex over one QUIC stream, for an extended-CONNECT tunnel.
|
|
348
|
+
*
|
|
349
|
+
* The same shape h2_server builds over an HTTP/2 stream, and for the same
|
|
350
|
+
* reason: ws_server was written against a socket, so handing it one of these
|
|
351
|
+
* is the whole integration — the WebSocket framing never learns whether its
|
|
352
|
+
* bytes are TCP, HTTP/2 DATA frames, or QUIC stream bytes.
|
|
353
|
+
*
|
|
354
|
+
* What QUIC adds for free is worth naming: each of these is an independent
|
|
355
|
+
* QUIC stream, so a WebSocket that stalls does not hold up the others on the
|
|
356
|
+
* connection. Over HTTP/2 they all share one TCP byte stream and it does.
|
|
357
|
+
*/
|
|
358
|
+
function _createTunnelDuplex(conn, streamId, req) {
|
|
359
|
+
var accepted = false;
|
|
360
|
+
|
|
361
|
+
var duplex = new Duplex({
|
|
362
|
+
read: function () {},
|
|
363
|
+
// Destroyed by the application (ws.terminate(), socket.destroy()) while
|
|
364
|
+
// the tunnel is open: the stream cancelled both ways. It was left open
|
|
365
|
+
// — the peer never heard the tunnel had gone. Destroyed because the
|
|
366
|
+
// stream already ended (releaseHttpStream), it is no longer listed.
|
|
367
|
+
destroy: function (err, cb) {
|
|
368
|
+
if (conn.http_streams[streamId] && conn.http_streams[streamId].tunnel === duplex) {
|
|
369
|
+
if (conn.h3) conn.h3.abortStream(streamId, err ? H3_ERR.H3_INTERNAL_ERROR : H3_ERR.H3_REQUEST_CANCELLED);
|
|
370
|
+
releaseHttpStream(conn, streamId, false);
|
|
371
|
+
}
|
|
372
|
+
cb(err);
|
|
373
|
+
},
|
|
374
|
+
write: function (chunk, encoding, cb) {
|
|
375
|
+
if (typeof chunk === 'string') chunk = Buffer.from(chunk, encoding);
|
|
376
|
+
var u8 = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
377
|
+
if (conn.h3) conn.h3.sendTunnelData(streamId, u8, false);
|
|
378
|
+
// Backpressure, as for a response (whenStreamSent in quic_connection).
|
|
379
|
+
if (conn.quic) conn.quic.whenStreamSent(Number(streamId), duplex.writableHighWaterMark, cb); else cb();
|
|
380
|
+
},
|
|
381
|
+
final: function (cb) {
|
|
382
|
+
if (conn.h3) conn.h3.sendTunnelData(streamId, new Uint8Array(0), true);
|
|
383
|
+
cb();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
duplex._streamId = streamId;
|
|
388
|
+
duplex.encrypted = true; // QUIC is always encrypted
|
|
389
|
+
duplex.setNoDelay = function () { return duplex; };
|
|
390
|
+
duplex.setKeepAlive = function () { return duplex; };
|
|
391
|
+
duplex.setTimeout = function () { return duplex; };
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Accept the tunnel.
|
|
395
|
+
*
|
|
396
|
+
* HTTP/3 has no 101 any more than HTTP/2 does: a 2xx accepts (RFC 9220
|
|
397
|
+
* §3). The headers go out WITHOUT FIN — the stream stays open in both
|
|
398
|
+
* directions, which is the entire point — and only then does the stream
|
|
399
|
+
* leave HTTP/3 framing.
|
|
400
|
+
*/
|
|
401
|
+
duplex._h2Accept = function (extraHeaders) {
|
|
402
|
+
if (accepted) return;
|
|
403
|
+
accepted = true;
|
|
404
|
+
var h = { ':status': '200' };
|
|
405
|
+
for (var n in (extraHeaders || {})) h[String(n).toLowerCase()] = extraHeaders[n];
|
|
406
|
+
if (conn.h3) {
|
|
407
|
+
conn.h3.sendHeaders(streamId, h, false);
|
|
408
|
+
conn.h3.registerTunnelStream(streamId);
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
/** Refuse it, with a status. */
|
|
413
|
+
duplex._h2Reject = function (status, message) {
|
|
414
|
+
if (accepted) return;
|
|
415
|
+
accepted = true;
|
|
416
|
+
if (conn.h3) {
|
|
417
|
+
conn.h3.sendHeaders(streamId, { ':status': String(status) }, true);
|
|
418
|
+
// Whatever the client sent ahead of our answer is held for a tunnel
|
|
419
|
+
// that will now never open: drop it and stop reading.
|
|
420
|
+
conn.h3.stopReading(streamId, H3_ERR.H3_REQUEST_REJECTED);
|
|
421
|
+
}
|
|
422
|
+
releaseHttpStream(conn, streamId, false);
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
return duplex;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
// ============================================================
|
|
430
|
+
// CONNECT-UDP over HTTP/3 — the carrier (see connect_udp.js)
|
|
431
|
+
//
|
|
432
|
+
// UDP payloads travel as HTTP Datagrams: a QUIC DATAGRAM frame each —
|
|
433
|
+
// Quarter Stream ID, Context ID 0, the payload — when both sides
|
|
434
|
+
// negotiated them (SETTINGS_H3_DATAGRAM, and QUIC's max_datagram_frame_
|
|
435
|
+
// size); otherwise as DATAGRAM capsules in the stream's DATA frames. A
|
|
436
|
+
// payload larger than a datagram can carry is dropped, not moved to the
|
|
437
|
+
// stream (RFC 9298 §5: "SHOULD NOT send the UDP payload in a DATAGRAM
|
|
438
|
+
// capsule") — a reliable, ordered copy of a UDP packet is not one.
|
|
439
|
+
// ============================================================
|
|
440
|
+
|
|
441
|
+
var UDP_HELD_MAX = 64; // datagrams waiting for the 2xx to leave
|
|
442
|
+
|
|
443
|
+
function startConnectUdp(conn, streamId, stream) {
|
|
444
|
+
var sid = Number(streamId);
|
|
445
|
+
var quarter = Math.floor(sid / 4);
|
|
446
|
+
var accepted = false, closed = false, busy = false;
|
|
447
|
+
var held = null; // until the 2xx has left: a datagram before it is one the client drops
|
|
448
|
+
|
|
449
|
+
function datagramsOn() {
|
|
450
|
+
return !!(conn.h3 && conn.h3.context.remote_datagram_support === true && conn.quic.maxDatagramSize() > 0);
|
|
451
|
+
}
|
|
452
|
+
function done() {
|
|
453
|
+
if (conn.http_streams[streamId] === stream) releaseHttpStream(conn, streamId, false);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
var carrier = {
|
|
457
|
+
httpVersion: '3.0',
|
|
458
|
+
connKey: conn,
|
|
459
|
+
sink: null, // set by the proxy: datagram(u8), data(u8), end(), abort()
|
|
460
|
+
peerEnded: false, // the client's FIN came
|
|
461
|
+
mode: function () { return datagramsOn() ? 'datagram' : 'capsule'; },
|
|
462
|
+
|
|
463
|
+
accept: function (headers) {
|
|
464
|
+
if (accepted || closed || !conn.h3) return;
|
|
465
|
+
accepted = true;
|
|
466
|
+
var h = { ':status': '200' };
|
|
467
|
+
for (var n in (headers || {})) h[n] = String(headers[n]);
|
|
468
|
+
conn.h3.sendHeaders(streamId, h, false);
|
|
469
|
+
held = [];
|
|
470
|
+
conn.quic.whenStreamSent(sid, 0, function () {
|
|
471
|
+
var list = held; held = null;
|
|
472
|
+
for (var i = 0; list && i < list.length; i++) conn.quic.sendDatagram(list[i], null, null);
|
|
473
|
+
});
|
|
474
|
+
},
|
|
475
|
+
|
|
476
|
+
reject: function (status, headers) {
|
|
477
|
+
if (accepted || closed || !conn.h3) return;
|
|
478
|
+
closed = true;
|
|
479
|
+
var h = { ':status': String(status) };
|
|
480
|
+
for (var n in (headers || {})) h[n] = String(headers[n]);
|
|
481
|
+
conn.h3.sendHeaders(streamId, h, true);
|
|
482
|
+
if (!carrier.peerEnded) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
|
|
483
|
+
done();
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
sendPayload: function (u8) {
|
|
487
|
+
if (!accepted || closed || !conn.h3) return false;
|
|
488
|
+
if (datagramsOn()) {
|
|
489
|
+
var ctx = contextZero(u8);
|
|
490
|
+
var q = writeVarInt(quarter);
|
|
491
|
+
var dg = new Uint8Array(q.byteLength + ctx.byteLength);
|
|
492
|
+
dg.set(q, 0); dg.set(ctx, q.byteLength);
|
|
493
|
+
if (dg.byteLength > conn.quic.maxDatagramSize()) return false;
|
|
494
|
+
if (held) { if (held.length >= UDP_HELD_MAX) return false; held.push(dg); return true; }
|
|
495
|
+
return conn.quic.sendDatagram(dg, null, null);
|
|
496
|
+
}
|
|
497
|
+
// Capsules: as much waiting on the stream as STREAM_QUEUE_MAX, and
|
|
498
|
+
// past it dropped, as a full UDP queue drops.
|
|
499
|
+
if (busy) return false;
|
|
500
|
+
conn.h3.sendBody(streamId, datagramCapsule(u8), false);
|
|
501
|
+
busy = true;
|
|
502
|
+
conn.quic.whenStreamSent(sid, STREAM_QUEUE_MAX, function () { busy = false; });
|
|
503
|
+
return true;
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
// Ours ends: FIN — and, if the client has not ended its side, it is told
|
|
507
|
+
// to stop sending (H3_NO_ERROR: nothing went wrong). Over.
|
|
508
|
+
end: function () {
|
|
509
|
+
if (closed || !conn.h3) return;
|
|
510
|
+
closed = true;
|
|
511
|
+
if (!accepted) conn.h3.sendHeaders(streamId, { ':status': '503' }, true);
|
|
512
|
+
else conn.h3.sendBody(streamId, null, true);
|
|
513
|
+
if (!carrier.peerEnded) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
|
|
514
|
+
done();
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// Broken: the stream reset both ways (H3_CONNECT_ERROR, RFC 9114 §4.4).
|
|
518
|
+
abort: function () {
|
|
519
|
+
if (closed) return;
|
|
520
|
+
closed = true;
|
|
521
|
+
if (conn.h3 && conn.http_streams[streamId] === stream) {
|
|
522
|
+
try { conn.h3.abortStream(streamId, H3_ERR.H3_CONNECT_ERROR); } catch (e) {}
|
|
523
|
+
}
|
|
524
|
+
done();
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
stream.udp = carrier;
|
|
529
|
+
// The stream's bytes are capsules, credited as they arrive (as a
|
|
530
|
+
// WebTransport session's).
|
|
531
|
+
if (conn.h3) conn.h3.releaseBody(sid);
|
|
532
|
+
conn.udpByQuarter[quarter] = sid;
|
|
533
|
+
connectUdp.handle(stream.req, carrier);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
|
|
102
538
|
// ============================================================
|
|
103
539
|
// set_http_stream — reactive HTTP stream builder
|
|
104
540
|
// Creates req/res on first call, fires handler when ready.
|
|
@@ -110,8 +546,21 @@ function createServer(options, handler) {
|
|
|
110
546
|
|
|
111
547
|
var is_new = false;
|
|
112
548
|
|
|
113
|
-
// Create req/res on first encounter of this stream
|
|
549
|
+
// Create req/res on first encounter of this stream — and only for its
|
|
550
|
+
// HEADERS. Body bytes or an end for a stream with no entry belong to a
|
|
551
|
+
// request that does not exist (never started, already cancelled, or
|
|
552
|
+
// already done); creating one for them built a request with no method
|
|
553
|
+
// and no URL, called the handler, and left an entry nothing would remove.
|
|
114
554
|
if (!(streamId in conn.http_streams)) {
|
|
555
|
+
if (!params || typeof params !== 'object' || !('request_headers' in params)) return;
|
|
556
|
+
|
|
557
|
+
// After GOAWAY: a request at or above its stream id is not processed
|
|
558
|
+
// (§5.2) — refused with H3_REQUEST_REJECTED, which tells the client it
|
|
559
|
+
// may safely retry it on another connection.
|
|
560
|
+
if (conn.goaway && Number(streamId) >= conn.goaway.id) {
|
|
561
|
+
if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_REQUEST_REJECTED);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
115
564
|
|
|
116
565
|
var req = {
|
|
117
566
|
method: null,
|
|
@@ -122,9 +571,29 @@ function createServer(options, handler) {
|
|
|
122
571
|
_listeners: {},
|
|
123
572
|
_isWebTransport: false,
|
|
124
573
|
|
|
574
|
+
// The connection's socket — one object for every request on it, as
|
|
575
|
+
// over TCP (see makeConnSocket).
|
|
576
|
+
h3Fingerprint: function () {
|
|
577
|
+
return conn.getH3Fingerprint ? conn.getH3Fingerprint() : null;
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
socket: conn.socket,
|
|
581
|
+
|
|
125
582
|
on: function (event, fn) {
|
|
126
583
|
(req._listeners[event] = req._listeners[event] || []).push(fn);
|
|
127
|
-
}
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
// The body's flow-control credit, returned as the layer above reads
|
|
587
|
+
// it rather than as it arrives (h3's holdBody): what that layer has
|
|
588
|
+
// not read yet bounds what the client may send.
|
|
589
|
+
_holdBody: function () { if (conn.h3) conn.h3.holdBody(streamId); },
|
|
590
|
+
_creditBody: function (n) { if (conn.h3) conn.h3.creditBody(streamId, n); },
|
|
591
|
+
_releaseBody: function () { if (conn.h3) conn.h3.releaseBody(streamId); },
|
|
592
|
+
|
|
593
|
+
// Set by the layer above: whether it still wants the rest of the body
|
|
594
|
+
// once the response is complete (see res.end). Unset, the rest is
|
|
595
|
+
// wanted if anything listens for it.
|
|
596
|
+
_wantsRest: null
|
|
128
597
|
};
|
|
129
598
|
|
|
130
599
|
var res = {
|
|
@@ -132,9 +601,23 @@ function createServer(options, handler) {
|
|
|
132
601
|
headers: {},
|
|
133
602
|
headersSent: false,
|
|
134
603
|
_finished: false,
|
|
135
|
-
|
|
604
|
+
// A response to HEAD, or a 204 or 304, has no content (RFC 9110 §9.3.2,
|
|
605
|
+
// §15.3.5, §15.4.5): what the application writes is dropped, as Node
|
|
606
|
+
// does over HTTP/1.1 and HTTP/2 (and these layers did already). Sent,
|
|
607
|
+
// it is DATA a client must reject: nghttp3 (curl, the h3 checkers)
|
|
608
|
+
// failed the request with ERR_MALFORMED_HTTP_MESSAGING. Set at
|
|
609
|
+
// writeHead, when the status is known.
|
|
610
|
+
_bodyAllowed: true,
|
|
611
|
+
|
|
612
|
+
// A list is kept a list, and goes as one field line per value, as
|
|
613
|
+
// HTTP/1.1 and HTTP/2 send it here and as Node's http does. It was
|
|
614
|
+
// String(list): two cookies became one "a=1,b=2", which a browser
|
|
615
|
+
// reads as a single cookie a with the value "1,b=2" (Set-Cookie
|
|
616
|
+
// never combines, RFC 9110 §5.3).
|
|
136
617
|
setHeader: function (name, value) {
|
|
137
|
-
|
|
618
|
+
validateHeaderName(name);
|
|
619
|
+
validateHeaderValue(name, value);
|
|
620
|
+
res.headers[name.toLowerCase()] = Array.isArray(value) ? value.map(String) : String(value);
|
|
138
621
|
},
|
|
139
622
|
|
|
140
623
|
getHeader: function (name) {
|
|
@@ -143,18 +626,28 @@ function createServer(options, handler) {
|
|
|
143
626
|
|
|
144
627
|
writeHead: function (statusCode, headers) {
|
|
145
628
|
if (res.headersSent) return res;
|
|
629
|
+
validateStatus(statusCode);
|
|
146
630
|
res.statusCode = statusCode;
|
|
631
|
+
res._bodyAllowed = !statusHasNoBody(statusCode) && req.headers[':method'] !== 'HEAD';
|
|
147
632
|
|
|
148
633
|
if (typeof headers === 'object') {
|
|
149
|
-
for (var name in headers)
|
|
150
|
-
res.headers[name.toLowerCase()] = String(headers[name]);
|
|
151
|
-
}
|
|
634
|
+
for (var name in headers) res.setHeader(name, headers[name]);
|
|
152
635
|
}
|
|
153
636
|
|
|
637
|
+
// Connection-specific fields are dropped, as HTTP/2's responses
|
|
638
|
+
// drop them (buildResponsePseudoHeaders): RFC 9114 §4.2 makes a
|
|
639
|
+
// message carrying one malformed, and a client may fail the request
|
|
640
|
+
// over it — an application's `Connection: close` or a WebSocket
|
|
641
|
+
// `Upgrade` meant for HTTP/1.1 cost the response. nginx fixed the
|
|
642
|
+
// same in 1.31.3.
|
|
154
643
|
var h3Headers = { ':status': String(statusCode) };
|
|
155
644
|
for (var name in res.headers) {
|
|
645
|
+
if (isConnectionSpecific(name) || name.charCodeAt(0) === 58) continue;
|
|
156
646
|
h3Headers[name] = res.headers[name];
|
|
157
647
|
}
|
|
648
|
+
// Date (RFC 9110 §6.6.1), as HTTP/2 and HTTP/1.1 send it — HTTP/3's
|
|
649
|
+
// responses had none.
|
|
650
|
+
if (!('date' in h3Headers) && statusCode >= 200 && !(res._wrapper && res._wrapper.sendDate === false)) h3Headers['date'] = httpDate();
|
|
158
651
|
|
|
159
652
|
if (conn.h3) {
|
|
160
653
|
// Send headers WITHOUT FIN for WT (session stays open)
|
|
@@ -163,70 +656,291 @@ function createServer(options, handler) {
|
|
|
163
656
|
}
|
|
164
657
|
res.headersSent = true;
|
|
165
658
|
|
|
659
|
+
// A WT CONNECT turned down: streams it already had are refused.
|
|
660
|
+
if (req.headers[':protocol'] === 'webtransport' && !(statusCode >= 200 && statusCode < 300) && conn.h3) {
|
|
661
|
+
conn.h3.rejectWebTransportSession(Number(streamId));
|
|
662
|
+
}
|
|
166
663
|
// If this is a WT CONNECT with 200 → register session
|
|
167
664
|
if (req.headers[':protocol'] === 'webtransport' && statusCode === 200 && conn.h3) {
|
|
168
665
|
req._isWebTransport = true;
|
|
666
|
+
req._wtSched = wtScheduling(conn.quic, Number(streamId));
|
|
667
|
+
// The session as wt_stream's createDatagrams / sessionStats take
|
|
668
|
+
// it — the client's WebTransport hands in the same.
|
|
669
|
+
req._wtOwner = { maxAge: null, expired: 0 };
|
|
670
|
+
req._wtSession = {
|
|
671
|
+
quic: conn.quic,
|
|
672
|
+
connected: function () { return req._isWebTransport && !req._wtClosed; },
|
|
673
|
+
send: function (u8, options) { return res.sendDatagram(u8, options); },
|
|
674
|
+
maxDatagramSize: function () { return res.maxDatagramSize(); },
|
|
675
|
+
owner: req._wtOwner,
|
|
676
|
+
dgStats: { droppedIncoming: 0, expiredIncoming: 0 }
|
|
677
|
+
};
|
|
169
678
|
conn.h3.registerWebTransportSession(Number(streamId));
|
|
679
|
+
// Its datagrams carry Number(streamId) / 4 (RFC 9297 §2.1).
|
|
680
|
+
conn.wtByQuarter[Math.floor(Number(streamId) / 4)] = Number(streamId);
|
|
681
|
+
// From here the CONNECT stream carries capsules, not a body: read
|
|
682
|
+
// here (onSessionCapsules), and credited as they arrive.
|
|
683
|
+
conn.h3.releaseBody(Number(streamId));
|
|
684
|
+
req._capsules = createCapsuleReader(function (info) { sessionClosed(info, false); },
|
|
685
|
+
function () { _emitOnReq(req, 'draining'); });
|
|
686
|
+
// Datagrams wait until the 200 has left (see sendDatagram).
|
|
687
|
+
req._wtDatagramsHeld = [];
|
|
688
|
+
conn.quic.whenStreamSent(Number(streamId), 0, function () {
|
|
689
|
+
var held = req._wtDatagramsHeld;
|
|
690
|
+
req._wtDatagramsHeld = null;
|
|
691
|
+
for (var i = 0; held && i < held.length; i++) conn.quic.sendDatagram(held[i].payload, held[i].prio, req._wtOwner);
|
|
692
|
+
});
|
|
170
693
|
}
|
|
171
694
|
|
|
172
695
|
return res;
|
|
173
696
|
},
|
|
174
697
|
|
|
698
|
+
// 103 Early Hints (RFC 8297): a HEADERS frame with :status 103 and
|
|
699
|
+
// no FIN — the final response follows on the same stream. A list of
|
|
700
|
+
// links goes as one field, comma-joined, which Link allows (RFC 8288).
|
|
701
|
+
writeEarlyHints: function (hints, callback) {
|
|
702
|
+
if (!res.headersSent && !res._finished && conn.h3) {
|
|
703
|
+
var h = { ':status': '103' };
|
|
704
|
+
for (var n in (hints || {})) {
|
|
705
|
+
if (!Object.prototype.hasOwnProperty.call(hints, n) || hints[n] == null) continue;
|
|
706
|
+
h[String(n).toLowerCase()] = Array.isArray(hints[n]) ? hints[n].join(', ') : String(hints[n]);
|
|
707
|
+
}
|
|
708
|
+
try { conn.h3.sendHeaders(streamId, h, false); } catch (e) { /* stream gone */ }
|
|
709
|
+
}
|
|
710
|
+
if (typeof callback === 'function') process.nextTick(callback);
|
|
711
|
+
},
|
|
712
|
+
|
|
713
|
+
// 102 Processing (node:http's writeProcessing): as Early Hints go.
|
|
714
|
+
writeProcessing: function () {
|
|
715
|
+
if (res.headersSent || res._finished || !conn.h3) return;
|
|
716
|
+
try { conn.h3.sendHeaders(streamId, { ':status': '102' }, false); } catch (e) { /* stream gone */ }
|
|
717
|
+
},
|
|
718
|
+
|
|
175
719
|
write: function (chunk) {
|
|
176
720
|
if (!res.headersSent) res.writeHead(res.statusCode);
|
|
721
|
+
if (!res._bodyAllowed) return true;
|
|
177
722
|
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
178
723
|
if (conn.h3) conn.h3.sendBody(streamId, chunk, false);
|
|
179
724
|
return true;
|
|
180
725
|
},
|
|
181
726
|
|
|
182
|
-
|
|
727
|
+
// Backpressure: cb once the stream has sent all but `bytes` of what
|
|
728
|
+
// was written (see whenStreamSent in quic_connection).
|
|
729
|
+
whenSent: function (bytes, cb) {
|
|
730
|
+
if (!conn.quic || !conn.quic.whenStreamSent) { cb(); return; }
|
|
731
|
+
conn.quic.whenStreamSent(Number(streamId), bytes, cb);
|
|
732
|
+
},
|
|
733
|
+
|
|
734
|
+
// `trailers`: fields after the body (res.addTrailers) — a HEADERS
|
|
735
|
+
// frame closing the stream (RFC 9114 §4.1). What gRPC answers with:
|
|
736
|
+
// grpc-status and grpc-message come only there.
|
|
737
|
+
end: function (chunk, trailers) {
|
|
183
738
|
if (res._finished) return;
|
|
184
739
|
if (!res.headersSent) res.writeHead(res.statusCode);
|
|
740
|
+
if (!res._bodyAllowed) { chunk = null; trailers = null; }
|
|
741
|
+
trailers = trailerFields(trailers);
|
|
742
|
+
var withTrailers = !!trailers;
|
|
185
743
|
if (conn.h3) {
|
|
186
744
|
if (chunk) {
|
|
187
745
|
if (typeof chunk === 'string') chunk = new TextEncoder().encode(chunk);
|
|
188
|
-
conn.h3.sendBody(streamId, chunk,
|
|
189
|
-
} else {
|
|
746
|
+
conn.h3.sendBody(streamId, chunk, !withTrailers);
|
|
747
|
+
} else if (!withTrailers) {
|
|
190
748
|
conn.h3.sendBody(streamId, null, true);
|
|
191
749
|
}
|
|
750
|
+
if (withTrailers) conn.h3.sendHeaders(streamId, trailers, true);
|
|
192
751
|
}
|
|
193
752
|
res._finished = true;
|
|
753
|
+
|
|
754
|
+
// The response is complete and the request is not (RFC 9114
|
|
755
|
+
// §4.1.2): a 413, a 401, a refusal answered before the upload is
|
|
756
|
+
// read. Unless something still reads the body, the client is told
|
|
757
|
+
// to stop sending it — STOP_SENDING with H3_NO_ERROR, which does
|
|
758
|
+
// not touch the response — and the request is over. It used to
|
|
759
|
+
// stay open: the client went on uploading into a window nobody
|
|
760
|
+
// would open again, holding its stream until it gave up.
|
|
761
|
+
if (!req._ended && !req._isWebTransport && conn.h3 && conn.http_streams[streamId] && !conn.http_streams[streamId].tunnel) {
|
|
762
|
+
var wants = typeof req._wantsRest === 'function' ? req._wantsRest()
|
|
763
|
+
: !!(req._listeners['data'] && req._listeners['data'].length);
|
|
764
|
+
if (!wants) {
|
|
765
|
+
conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
|
|
766
|
+
releaseHttpStream(conn, streamId, false);
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
releaseIfDone(conn, streamId);
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
// The application gave the exchange up (res.destroy(), req.destroy()
|
|
774
|
+
// — unified server): the response not finished, the stream is
|
|
775
|
+
// cancelled both ways (RFC 9114 §4.1.1), `code` H3_REQUEST_CANCELLED
|
|
776
|
+
// or, for a failure of ours, H3_INTERNAL_ERROR; the response already
|
|
777
|
+
// complete, only what is left of the request body is refused
|
|
778
|
+
// (STOP_SENDING H3_NO_ERROR, §4.1.2), the response left to arrive.
|
|
779
|
+
// The request is then over here, as at any other end. Without this
|
|
780
|
+
// the client waited for the rest of a response that would never come
|
|
781
|
+
// and the stream stayed listed until the connection closed.
|
|
782
|
+
abort: function (code) {
|
|
783
|
+
if (!conn.http_streams[streamId]) return;
|
|
784
|
+
if (!res._finished) {
|
|
785
|
+
res._finished = true;
|
|
786
|
+
if (conn.h3) conn.h3.abortStream(streamId, code);
|
|
787
|
+
} else if (!req._ended) {
|
|
788
|
+
if (conn.h3) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
|
|
789
|
+
} else {
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
releaseHttpStream(conn, streamId, false);
|
|
793
|
+
},
|
|
794
|
+
|
|
795
|
+
// The response's priority, as the server decides it: { urgency
|
|
796
|
+
// (0..7, 0 first), incremental } — RFC 9218's parameters. It replaces
|
|
797
|
+
// what the client asked for (its `priority` header, PRIORITY_UPDATE),
|
|
798
|
+
// and later requests from the client do not override it. For a
|
|
799
|
+
// WebTransport session, its urgency is its streams' too.
|
|
800
|
+
setPriority: function (p) {
|
|
801
|
+
if (conn.quic) conn.quic.setStreamPriority(Number(streamId), responsePriority(p));
|
|
194
802
|
},
|
|
195
803
|
|
|
196
804
|
// ---- WebTransport methods on res ----
|
|
197
805
|
|
|
198
|
-
|
|
199
|
-
|
|
806
|
+
// true: queued, and sent if room comes in time (quic_connection's
|
|
807
|
+
// sendDatagram — never retransmitted, the oldest dropped first when
|
|
808
|
+
// the path cannot keep up). false: it cannot be — no session, or
|
|
809
|
+
// larger than the peer takes (res.maxDatagramSize()).
|
|
810
|
+
//
|
|
811
|
+
// Held until the 200 that opens the session has left: a datagram the
|
|
812
|
+
// browser receives before it knows the session exists is dropped.
|
|
813
|
+
// Sent at once, they all were — every one the application sent right
|
|
814
|
+
// after accepting the session, 3000 of 3000, since the 200 waits for
|
|
815
|
+
// the end of the tick and a datagram did not.
|
|
816
|
+
//
|
|
817
|
+
// options { sendOrder, sendGroup }: as the browser's
|
|
818
|
+
// datagrams.createWritable(options) — its place among the session's
|
|
819
|
+
// streams (wtScheduling); without them, ahead of every stream.
|
|
820
|
+
sendDatagram: function (data, options) {
|
|
821
|
+
if (!req._isWebTransport || !conn.quic) return false;
|
|
200
822
|
if (typeof data === 'string') data = Buffer.from(data);
|
|
201
823
|
if (Buffer.isBuffer(data)) data = new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
|
|
202
|
-
|
|
824
|
+
var payload = wt_datagram_payload(Number(streamId), data);
|
|
825
|
+
if (payload.byteLength > conn.quic.maxDatagramSize()) return false;
|
|
826
|
+
var prio = req._wtSched.forDatagram(options);
|
|
827
|
+
if (req._wtDatagramsHeld) {
|
|
828
|
+
req._wtDatagramsHeld.push({ payload: payload, prio: prio });
|
|
829
|
+
if (req._wtDatagramsHeld.length > WT_DATAGRAMS_HELD) req._wtDatagramsHeld.shift(); // the oldest goes
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
return conn.quic.sendDatagram(payload, prio, req._wtOwner);
|
|
203
833
|
},
|
|
204
834
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return
|
|
835
|
+
// The largest datagram this session can send, in bytes of payload.
|
|
836
|
+
maxDatagramSize: function () {
|
|
837
|
+
if (!req._isWebTransport || !conn.quic) return 0;
|
|
838
|
+
return Math.max(0, conn.quic.maxDatagramSize() - (wt_datagram_payload(Number(streamId), new Uint8Array(0)).byteLength));
|
|
209
839
|
},
|
|
210
840
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return
|
|
841
|
+
// Close the session with a code (32-bit) and a reason (UTF-8, up to
|
|
842
|
+
// 1024 bytes): CLOSE_WEBTRANSPORT_SESSION, then the CONNECT stream's
|
|
843
|
+
// end — what a browser's wt.close({ closeCode, reason }) sends, and
|
|
844
|
+
// what its wt.closed resolves with. Its streams are reset with
|
|
845
|
+
// WEBTRANSPORT_SESSION_GONE. There was no way to close a session but
|
|
846
|
+
// to close the whole QUIC connection under it.
|
|
847
|
+
closeSession: function (code, reason) {
|
|
848
|
+
if (!req._isWebTransport || req._wtClosed || !conn.h3) return;
|
|
849
|
+
conn.h3.sendBody(streamId, wtCloseCapsule(code || 0, reason), true);
|
|
850
|
+
sessionClosed({ closeCode: Number(code) || 0, reason: reason == null ? '' : String(reason) }, true);
|
|
851
|
+
},
|
|
852
|
+
|
|
853
|
+
// DRAIN_WEBTRANSPORT_SESSION: the peer should finish what it is doing
|
|
854
|
+
// and start nothing new; the session stays open.
|
|
855
|
+
drainSession: function () {
|
|
856
|
+
if (!req._isWebTransport || req._wtClosed || !conn.h3) return;
|
|
857
|
+
conn.h3.sendBody(streamId, wtDrainCapsule(), false);
|
|
858
|
+
},
|
|
859
|
+
|
|
860
|
+
// The application protocol chosen for the session, one of those the
|
|
861
|
+
// client offered (req.webTransportProtocols): sent as WT-Protocol
|
|
862
|
+
// with the 200, and what the browser's wt.protocol then says. Before
|
|
863
|
+
// writeHead; a name the client did not offer throws — the client
|
|
864
|
+
// would fail the session over it.
|
|
865
|
+
setWebTransportProtocol: function (name) {
|
|
866
|
+
if (res.headersSent) throw new Error('setWebTransportProtocol() after the response head was sent');
|
|
867
|
+
var offered = req.webTransportProtocols || [];
|
|
868
|
+
if (offered.indexOf(name) < 0) throw new TypeError('the client did not offer the protocol ' + JSON.stringify(name));
|
|
869
|
+
res.setHeader('wt-protocol', sfString(name));
|
|
870
|
+
},
|
|
871
|
+
|
|
872
|
+
// The browser's getStats(), as the client's wt.getStats(): the
|
|
873
|
+
// connection's figures and this session's datagrams (wt_stream's
|
|
874
|
+
// sessionStats).
|
|
875
|
+
getStats: function () {
|
|
876
|
+
if (!req._wtSession) return Promise.resolve(null);
|
|
877
|
+
return sessionStats(req._wtSession);
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
// The browser's WebTransportSendGroup, for a stream's sendGroup or a
|
|
881
|
+
// datagram's.
|
|
882
|
+
createSendGroup: function () { return createSendGroup(); },
|
|
883
|
+
|
|
884
|
+
// The browser's exportKeyingMaterial, as the client's: this session's
|
|
885
|
+
// keying material from the TLS exporter — the same bytes the client
|
|
886
|
+
// gets for the same label and context.
|
|
887
|
+
exportKeyingMaterial: function (label, context, outputLength) {
|
|
888
|
+
var a;
|
|
889
|
+
try { a = exporterArgs(arguments); } catch (e) { return Promise.reject(e); }
|
|
890
|
+
if (!req._isWebTransport || req._wtClosed) return Promise.reject(new DOMException('the session is closed', 'InvalidStateError'));
|
|
891
|
+
try { return Promise.resolve(exportSessionKeyingMaterial(conn.quic, Number(streamId), a)); }
|
|
892
|
+
catch (e) { return Promise.reject(e); }
|
|
893
|
+
},
|
|
894
|
+
|
|
895
|
+
// A stream of ours: a Duplex, or a Writable — recorded with its
|
|
896
|
+
// session, so the session's end destroys it. options { sendOrder,
|
|
897
|
+
// sendGroup, waitUntilAvailable }, the browser's (wt_stream's
|
|
898
|
+
// openWtStream): null, without waitUntilAvailable, when the client
|
|
899
|
+
// allows no more streams now (res.canOpenStream()).
|
|
900
|
+
//
|
|
901
|
+
// callback(err, stream): once it is open — at once, or (with
|
|
902
|
+
// waitUntilAvailable) when the client allows it; err if it closed
|
|
903
|
+
// first. stream.pending and its 'ready' say the same.
|
|
904
|
+
createBidirectionalStream: function (options, callback) { return openSessionStream(true, options, callback); },
|
|
905
|
+
createUnidirectionalStream: function (options, callback) { return openSessionStream(false, options, callback); },
|
|
906
|
+
|
|
907
|
+
// Whether a stream of ours could be opened now, within the client's
|
|
908
|
+
// MAX_STREAMS. options { bidirectional }.
|
|
909
|
+
canOpenStream: function (options) {
|
|
910
|
+
return !!(req._isWebTransport && !req._wtClosed && conn.quic && conn.quic.canOpenStream(!!(options && options.bidirectional)));
|
|
227
911
|
}
|
|
228
912
|
};
|
|
229
913
|
|
|
914
|
+
// The browser's datagrams object (readable, writable, createWritable,
|
|
915
|
+
// the age limits), as the client's wt.datagrams — made on first use.
|
|
916
|
+
Object.defineProperty(res, 'datagrams', { enumerable: true, get: function () {
|
|
917
|
+
if (!req._wtSession) return null;
|
|
918
|
+
return req._wtDatagrams || (req._wtDatagrams = createDatagrams(req._wtSession));
|
|
919
|
+
} });
|
|
920
|
+
|
|
921
|
+
var openSessionStream = function (bidi, options, callback) {
|
|
922
|
+
if (typeof options === 'function') { callback = options; options = null; }
|
|
923
|
+
if (!req._isWebTransport || req._wtClosed || !conn.h3) return null;
|
|
924
|
+
return openWtStream(conn.h3, conn.quic, Number(streamId), bidi, options, function (id, kind, prefix) {
|
|
925
|
+
return _createWtStream(conn, conn.quic, id, Number(streamId), kind, prefix);
|
|
926
|
+
}, callback);
|
|
927
|
+
};
|
|
928
|
+
|
|
929
|
+
// A WebTransport session ended by CLOSE_WEBTRANSPORT_SESSION — the
|
|
930
|
+
// peer's capsule, or ours (res.closeSession). req.closeInfo holds the
|
|
931
|
+
// code and reason, as a browser's wt.closed resolves with them; then
|
|
932
|
+
// 'close', and the session's streams go (WEBTRANSPORT_SESSION_GONE).
|
|
933
|
+
// After the peer's, our side of the CONNECT stream ends too (§5).
|
|
934
|
+
var sessionClosed = function (info, byUs) {
|
|
935
|
+
if (req._wtClosed || !req._isWebTransport) return;
|
|
936
|
+
req.closeInfo = info;
|
|
937
|
+
if (req._wrapper) req._wrapper.closeInfo = info;
|
|
938
|
+
req._capsules = null;
|
|
939
|
+
if (!byUs && conn.h3) conn.h3.sendBody(streamId, null, true);
|
|
940
|
+
if (conn.h3) conn.h3.endWebTransportSession(Number(streamId));
|
|
941
|
+
releaseHttpStream(conn, streamId, false);
|
|
942
|
+
};
|
|
943
|
+
|
|
230
944
|
conn.http_streams[streamId] = { req: req, res: res };
|
|
231
945
|
is_new = true;
|
|
232
946
|
}
|
|
@@ -239,6 +953,13 @@ function createServer(options, handler) {
|
|
|
239
953
|
for (var name in params.request_headers) {
|
|
240
954
|
stream.req.headers[name] = params.request_headers[name];
|
|
241
955
|
}
|
|
956
|
+
// The field lines as they came (validateFieldSection), which the
|
|
957
|
+
// copy above leaves behind — not enumerable: req.rawHeaders and
|
|
958
|
+
// headersDistinct are built from them. Without it they were rebuilt
|
|
959
|
+
// from the joined values, two x-a lines one "1, 2".
|
|
960
|
+
if (params.request_headers._rawHeaders && !stream.req.headers._rawHeaders) {
|
|
961
|
+
Object.defineProperty(stream.req.headers, '_rawHeaders', { value: params.request_headers._rawHeaders, enumerable: false });
|
|
962
|
+
}
|
|
242
963
|
|
|
243
964
|
// Extract :method and :path
|
|
244
965
|
if (stream.req.method === null && ':method' in stream.req.headers) {
|
|
@@ -247,10 +968,21 @@ function createServer(options, handler) {
|
|
|
247
968
|
if (stream.req.url === null && ':path' in stream.req.headers) {
|
|
248
969
|
stream.req.url = stream.req.headers[':path'];
|
|
249
970
|
}
|
|
971
|
+
// A WebTransport CONNECT: the application protocols the client
|
|
972
|
+
// offers (WT-Available-Protocols), in its order of preference.
|
|
973
|
+
if (stream.req.headers[':protocol'] === 'webtransport') {
|
|
974
|
+
stream.req.webTransportProtocols = parseSfStringList(stream.req.headers['wt-available-protocols']);
|
|
975
|
+
}
|
|
250
976
|
}
|
|
251
977
|
|
|
252
978
|
if ('request_body_chunk' in params) {
|
|
253
979
|
var stream = conn.http_streams[streamId];
|
|
980
|
+
// A 0-RTT request being judged (earlyGate): later.
|
|
981
|
+
if (stream.req._paused) { stream.req._paused.push({ request_body_chunk: params.request_body_chunk }); return; }
|
|
982
|
+
// An open WebTransport session's CONNECT stream: capsules, not a body.
|
|
983
|
+
if (stream.req._capsules) { stream.req._capsules(params.request_body_chunk); return; }
|
|
984
|
+
// A CONNECT-UDP tunnel's: capsules too, for the proxy.
|
|
985
|
+
if (stream.udp) { if (stream.udp.sink) stream.udp.sink.data(params.request_body_chunk); return; }
|
|
254
986
|
var listeners = stream.req._listeners['data'] || [];
|
|
255
987
|
for (var i = 0; i < listeners.length; i++) {
|
|
256
988
|
try { listeners[i](params.request_body_chunk); } catch (e) { }
|
|
@@ -259,21 +991,126 @@ function createServer(options, handler) {
|
|
|
259
991
|
|
|
260
992
|
if ('request_end' in params) {
|
|
261
993
|
var stream = conn.http_streams[streamId];
|
|
994
|
+
if (stream.req._paused) { stream.req._paused.push({ request_end: true }); return; }
|
|
995
|
+
clearRequestDeadline(stream);
|
|
996
|
+
// The client closed the tunnel (§3.1 of RFC 9298).
|
|
997
|
+
if (stream.udp) { stream.udp.peerEnded = true; if (stream.udp.sink) stream.udp.sink.end(); return; }
|
|
262
998
|
var listeners = stream.req._listeners['end'] || [];
|
|
263
999
|
for (var i = 0; i < listeners.length; i++) {
|
|
264
1000
|
try { listeners[i](); } catch (e) { }
|
|
265
1001
|
}
|
|
1002
|
+
stream.req._ended = true;
|
|
1003
|
+
|
|
1004
|
+
// A session ends with its CONNECT stream's FIN (releaseHttpStream
|
|
1005
|
+
// says so and closes its streams); a request, once its response is
|
|
1006
|
+
// done too.
|
|
1007
|
+
if (stream.req._isWebTransport) releaseHttpStream(conn, streamId, false);
|
|
1008
|
+
else releaseIfDone(conn, streamId);
|
|
266
1009
|
}
|
|
267
1010
|
}
|
|
268
1011
|
|
|
269
|
-
// Fire handler on new stream (after headers are set)
|
|
1012
|
+
// Fire handler on new stream (after headers are set). A request that
|
|
1013
|
+
// came in 0-RTT is judged first (earlyGate): it may be a replay.
|
|
270
1014
|
if (is_new) {
|
|
271
|
-
var
|
|
272
|
-
|
|
273
|
-
|
|
1015
|
+
var newStream = conn.http_streams[streamId];
|
|
1016
|
+
newStream.req.earlyData = !!(conn.quic && conn.quic.isEarlyStream && conn.quic.isEarlyStream(streamId));
|
|
1017
|
+
if (newStream.req.earlyData) { earlyGate(conn, streamId, newStream, function () { dispatchNewRequest(newStream); }); return; }
|
|
1018
|
+
dispatchNewRequest(newStream);
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function dispatchNewRequest(stream) {
|
|
1022
|
+
|
|
1023
|
+
// ---- Extended CONNECT for something other than WebTransport ----
|
|
1024
|
+
//
|
|
1025
|
+
// RFC 9220 bootstraps WebSocket over HTTP/3 exactly as RFC 8441 does
|
|
1026
|
+
// over HTTP/2: a CONNECT carrying :protocol, answered with a 2xx. The
|
|
1027
|
+
// two differ only in what runs on the stream afterwards — WebTransport
|
|
1028
|
+
// turns it into a session that later streams join, WebSocket just sends
|
|
1029
|
+
// frames on it — so a WebSocket must NOT be registered as a session, or
|
|
1030
|
+
// unrelated streams carrying its id would be mistaken for members of it.
|
|
1031
|
+
var proto = stream.req.headers[':protocol'];
|
|
1032
|
+
// CONNECT-UDP, when the application proxies UDP: not a tunnel of raw
|
|
1033
|
+
// bytes — after the 2xx the stream still carries HTTP/3 DATA frames,
|
|
1034
|
+
// holding capsules (RFC 9297 §3.2), as a WebTransport session's does.
|
|
1035
|
+
if (proto && connectUdp && String(proto).toLowerCase() === 'connect-udp' && connectUdp.wants(stream.req)) {
|
|
1036
|
+
startConnectUdp(conn, streamId, stream);
|
|
1037
|
+
return;
|
|
274
1038
|
}
|
|
275
|
-
|
|
1039
|
+
if (proto && String(proto).toLowerCase() !== 'webtransport' && shouldUpgradeCallback(stream.req)) {
|
|
1040
|
+
// Stop HTTP/3 framing on this stream NOW, before the application has
|
|
1041
|
+
// decided anything: a client may already be sending WebSocket frames,
|
|
1042
|
+
// and they must not reach the frame parser.
|
|
1043
|
+
if (conn.h3) conn.h3.beginTunnelStream(streamId);
|
|
1044
|
+
var duplex = _createTunnelDuplex(conn, streamId, stream.req);
|
|
1045
|
+
stream.tunnel = duplex;
|
|
1046
|
+
|
|
1047
|
+
if (upgradeListeners === 0) {
|
|
1048
|
+
// Accepted by shouldUpgradeCallback, yet nobody listens. Answer
|
|
1049
|
+
// rather than leave the peer waiting for a 2xx that will never come.
|
|
1050
|
+
if (conn.h3) {
|
|
1051
|
+
conn.h3.sendHeaders(streamId, { ':status': '501' }, true);
|
|
1052
|
+
conn.h3.stopReading(streamId, H3_ERR.H3_REQUEST_REJECTED);
|
|
1053
|
+
}
|
|
1054
|
+
releaseHttpStream(conn, streamId, false);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
stream.req.upgrade = true;
|
|
1058
|
+
ev.emit('upgrade', stream.req, duplex, Buffer.alloc(0));
|
|
1059
|
+
return;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
if (stream.req.method !== 'CONNECT' && !stream.req._ended) armRequestDeadline(conn, streamId, stream);
|
|
1063
|
+
|
|
1064
|
+
// A handler that throws loses its own stream — reset both ways,
|
|
1065
|
+
// H3_INTERNAL_ERROR — not the connection (see runRequestHandler). A
|
|
1066
|
+
// response it had already ended is left to finish.
|
|
1067
|
+
// maxRequestsPerSocket: after the Nth request, GOAWAY — those in flight
|
|
1068
|
+
// finish, the client takes the next to a new connection (as HTTP/2).
|
|
1069
|
+
if (maxRequests > 0 && ++conn.served === maxRequests && conn.handle) conn.handle.goaway();
|
|
1070
|
+
runRequestHandler(function () {
|
|
1071
|
+
if (typeof handler === 'function') handler(stream.req, stream.res);
|
|
1072
|
+
ev.emit('request', stream.req, stream.res);
|
|
1073
|
+
}, function () {
|
|
1074
|
+
var w = stream.res && stream.res._wrapper;
|
|
1075
|
+
if (w ? w.writableEnded : stream.res._finished) return;
|
|
1076
|
+
if (conn.http_streams[streamId] !== stream) return;
|
|
1077
|
+
if (conn.h3) conn.h3.abortStream(streamId, H3_ERR.H3_INTERNAL_ERROR);
|
|
1078
|
+
releaseHttpStream(conn, streamId, true);
|
|
1079
|
+
});
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
// ── 0-RTT requests (RFC 8470) ──────────────────────────────────────────
|
|
1084
|
+
//
|
|
1085
|
+
// A request that came in 0-RTT (req.earlyData) may be a replay: whoever
|
|
1086
|
+
// captured the client's first flight can send it again. Each one is asked
|
|
1087
|
+
// about first — earlyDataFilter(req, callback(err, allow)), the
|
|
1088
|
+
// application's, or by default: GET, HEAD and OPTIONS go on, anything else
|
|
1089
|
+
// is answered 425 Too Early, which a client sends again once its handshake
|
|
1090
|
+
// is done (the browsers do, and quico's client). No answer within
|
|
1091
|
+
// EARLY_FILTER_TIMEOUT_MS, an error, a throw: 425 — the request is not
|
|
1092
|
+
// lost, only later. Meanwhile what more arrives on the stream (a body, its
|
|
1093
|
+
// end) waits, for the handler to find.
|
|
1094
|
+
// (The filter and its timeout: http_common's runEarlyDataFilter.)
|
|
1095
|
+
function earlyGate(conn, streamId, stream, proceed) {
|
|
1096
|
+
var req = stream.req;
|
|
1097
|
+
req._paused = [];
|
|
1098
|
+
runEarlyDataFilter(earlyDataFilter, req, function (allow) {
|
|
1099
|
+
var queued = req._paused;
|
|
1100
|
+
req._paused = null;
|
|
1101
|
+
if (conn.http_streams[streamId] !== stream) return; // gone meanwhile (reset, closed)
|
|
1102
|
+
if (!allow) { tooEarly(conn, streamId, stream); return; }
|
|
1103
|
+
proceed();
|
|
1104
|
+
for (var i = 0; i < queued.length; i++) set_http_stream(conn, streamId, queued[i]);
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
function tooEarly(conn, streamId, stream) {
|
|
1108
|
+
if (options.stats) options.stats.earlyData.tooEarly++;
|
|
1109
|
+
if (conn.h3) {
|
|
1110
|
+
conn.h3.sendHeaders(streamId, { ':status': '425', 'content-length': '0' }, true);
|
|
1111
|
+
if (!stream.req._ended) conn.h3.stopReading(streamId, H3_ERR.H3_NO_ERROR);
|
|
276
1112
|
}
|
|
1113
|
+
releaseHttpStream(conn, streamId, false);
|
|
277
1114
|
}
|
|
278
1115
|
|
|
279
1116
|
|
|
@@ -286,19 +1123,355 @@ function createServer(options, handler) {
|
|
|
286
1123
|
// socket, demux and connection lifecycle.
|
|
287
1124
|
// ============================================================
|
|
288
1125
|
|
|
1126
|
+
/**
|
|
1127
|
+
* A stand-in for the TLSSocket that TCP requests carry — ONE per QUIC
|
|
1128
|
+
* connection, shared by every request on it.
|
|
1129
|
+
*
|
|
1130
|
+
* Over TCP, req.socket is the connection: every request on it gets the
|
|
1131
|
+
* same object, which is how an application tells that two requests came in
|
|
1132
|
+
* on one connection, and hears 'close' when it goes. It used to be built
|
|
1133
|
+
* per request here, so over HTTP/3 no two requests shared one and nothing
|
|
1134
|
+
* ever closed.
|
|
1135
|
+
*
|
|
1136
|
+
* There is no real socket: the TLS handshake happens inside the QUIC
|
|
1137
|
+
* connection, so what an application reaches for on req.socket is answered
|
|
1138
|
+
* from it. `encrypted` is always true — QUIC has no unencrypted mode.
|
|
1139
|
+
* The address follows the connection when the client moves (migration,
|
|
1140
|
+
* RFC 9000 §9) — once the new address is validated.
|
|
1141
|
+
*/
|
|
1142
|
+
function makeConnSocket(conn, quic, peer) {
|
|
1143
|
+
// A real Duplex (streams.js), not a bare emitter: Express reads
|
|
1144
|
+
// socket.readable / socket.writable to decide whether a request is still
|
|
1145
|
+
// open (on-finished — body-parser, express.static), and listens on it with
|
|
1146
|
+
// on/removeListener. Without them every HTTP/3 request looked finished
|
|
1147
|
+
// before it started: a POST body was skipped and a static file never sent.
|
|
1148
|
+
var sock = _makeSocketStub();
|
|
1149
|
+
sock.alpnProtocol = 'h3';
|
|
1150
|
+
// The connection's QUIC version (1, or 0x6b3343cf for v2) — after
|
|
1151
|
+
// compatible version negotiation, the one negotiated.
|
|
1152
|
+
Object.defineProperty(sock, 'quicVersion', { configurable: true, enumerable: true,
|
|
1153
|
+
get: function () { return quic.version; } });
|
|
1154
|
+
// Live, as net.Socket's are: a client whose NAT changes its port is
|
|
1155
|
+
// followed by the connection (RFC 9000 §9.3), and a copy taken here
|
|
1156
|
+
// would go on naming the address it left.
|
|
1157
|
+
Object.defineProperty(sock, 'remoteAddress', { configurable: true, enumerable: true,
|
|
1158
|
+
get: function () { return quic.remoteAddress || (peer && peer.address) || null; } });
|
|
1159
|
+
Object.defineProperty(sock, 'remotePort', { configurable: true, enumerable: true,
|
|
1160
|
+
get: function () { return quic.remotePort || (peer && peer.port) || null; } });
|
|
1161
|
+
// TLS fingerprints and the certificate presented, as on a TLSSocket.
|
|
1162
|
+
sock.getJA4 = function (opts) { return quic.getJA4 ? quic.getJA4(opts) : null; };
|
|
1163
|
+
sock.getJA5 = function (opts) { return quic.getJA5 ? quic.getJA5(opts) : null; };
|
|
1164
|
+
sock.getJA4S = function (opts) { return quic.getJA4S ? quic.getJA4S(opts) : null; };
|
|
1165
|
+
// What the handshake settled, as a lemon-tls TLSSocket answers over
|
|
1166
|
+
// HTTP/2 and HTTP/1.1 — among it whether the certificates went compressed.
|
|
1167
|
+
sock.getNegotiationResult = function () { return quic.getNegotiationResult ? quic.getNegotiationResult() : null; };
|
|
1168
|
+
sock.getCertificate = function () { return quic.getCertificate ? quic.getCertificate() : {}; };
|
|
1169
|
+
// The client's certificate and its verification, as a TLSSocket's
|
|
1170
|
+
// (requestCert). The stub said `authorized: true` and `{}` for every
|
|
1171
|
+
// connection: harmless while QUIC could not ask for a certificate, and
|
|
1172
|
+
// an open door once it could — an application checking
|
|
1173
|
+
// req.socket.authorized would have let in a client that sent none.
|
|
1174
|
+
Object.defineProperty(sock, 'authorized', { configurable: true, enumerable: true,
|
|
1175
|
+
get: function () { return !!(quic.isAuthorized && quic.isAuthorized()); } });
|
|
1176
|
+
Object.defineProperty(sock, 'authorizationError', { configurable: true, enumerable: true,
|
|
1177
|
+
get: function () { return quic.authorizationError ? quic.authorizationError() : null; } });
|
|
1178
|
+
sock.getPeerCertificate = function () { return quic.getPeerCertificate ? quic.getPeerCertificate() : {}; };
|
|
1179
|
+
sock.getPeerX509Certificate = function () {
|
|
1180
|
+
var c = sock.getPeerCertificate();
|
|
1181
|
+
try { return c && c.raw ? new X509Certificate(c.raw) : undefined; } catch (e) { return undefined; }
|
|
1182
|
+
};
|
|
1183
|
+
// The session ticket, { data, resumed, offered } — the same method a
|
|
1184
|
+
// TLSSocket has over TCP, so req.socket answers it on every HTTP version.
|
|
1185
|
+
sock.getTicketInfo = function () { return quic.getTicketInfo ? quic.getTicketInfo() : null; };
|
|
1186
|
+
// This connection's options while it runs (quic_connection's
|
|
1187
|
+
// setOptions): every request on the connection shares them.
|
|
1188
|
+
sock.setOptions = function (o) { quic.setOptions(o); return sock; };
|
|
1189
|
+
sock.getOptions = function () { return quic.getOptions(); };
|
|
1190
|
+
// And the rest of a TLSSocket's session questions.
|
|
1191
|
+
sock.isSessionReused = function () { return !!(quic.isSessionReused && quic.isSessionReused()); };
|
|
1192
|
+
sock.getProtocol = function () { return quic.getProtocol ? quic.getProtocol() : null; };
|
|
1193
|
+
sock.getCipher = function () { return quic.getCipher ? quic.getCipher() : null; };
|
|
1194
|
+
// The protocol-layer fingerprint, alongside the TLS one above. Named to
|
|
1195
|
+
// match req.h2Fingerprint on the TCP side so an application can reach for
|
|
1196
|
+
// whichever is present without branching on version.
|
|
1197
|
+
sock.h3Fingerprint = function () { return conn.getH3Fingerprint ? conn.getH3Fingerprint() : null; };
|
|
1198
|
+
// The SNI the client sent, or false — known once the ClientHello is in,
|
|
1199
|
+
// which is before any request, so read live rather than copied now.
|
|
1200
|
+
Object.defineProperty(sock, 'servername', {
|
|
1201
|
+
get: function () { return quic.servername || false; },
|
|
1202
|
+
enumerable: true
|
|
1203
|
+
});
|
|
1204
|
+
// Encrypted Client Hello, as over TCP (lemon-tls TLSSocket): null when
|
|
1205
|
+
// not offered, 'accepted' — servername is then the name that was hidden
|
|
1206
|
+
// — or 'rejected' (servername is the public name).
|
|
1207
|
+
Object.defineProperty(sock, 'echStatus', {
|
|
1208
|
+
get: function () { return quic.echStatus; },
|
|
1209
|
+
enumerable: true
|
|
1210
|
+
});
|
|
1211
|
+
Object.defineProperty(sock, 'echAccepted', {
|
|
1212
|
+
get: function () { return quic.echStatus === 'accepted'; },
|
|
1213
|
+
enumerable: true
|
|
1214
|
+
});
|
|
1215
|
+
// Gone with the connection: destroy() turns readable/writable off, sets
|
|
1216
|
+
// destroyed, and emits 'close' — as a socket does.
|
|
1217
|
+
quic.on('close', function () {
|
|
1218
|
+
if (!sock.destroyed) sock.destroy();
|
|
1219
|
+
});
|
|
1220
|
+
return sock;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Every connection, for closing them (closeIdleConnections, close()).
|
|
1224
|
+
var conns = new Set();
|
|
1225
|
+
var closingServer = null; // close()'s callback, once every connection is gone
|
|
1226
|
+
|
|
289
1227
|
qserver.on('connection', function (quic, peer) {
|
|
290
|
-
var conn = { quic: quic, h3: null, http_streams: {} }
|
|
1228
|
+
var conn = { quic: quic, h3: null, http_streams: {}, _wtStreamObjects: {}, served: 0,
|
|
1229
|
+
headerDeadlines: {}, // request streams whose HEADERS is still arriving
|
|
1230
|
+
wtByQuarter: {}, // WebTransport sessions by Quarter Stream ID → stream id
|
|
1231
|
+
udpByQuarter: {} }; // CONNECT-UDP tunnels, the same way
|
|
1232
|
+
conn.socket = makeConnSocket(conn, quic, peer);
|
|
1233
|
+
|
|
1234
|
+
// The connection is gone — CONNECTION_CLOSE from either side, or the
|
|
1235
|
+
// idle timeout. Every WebTransport session on it ends with it, and says
|
|
1236
|
+
// so with 'close' on its request, as a session the browser closes does.
|
|
1237
|
+
// Without this the application never heard of a peer that vanished.
|
|
1238
|
+
//
|
|
1239
|
+
// So does everything else still open on it. A request whose body was
|
|
1240
|
+
// still arriving, or whose response was still being written, or a
|
|
1241
|
+
// WebSocket tunnel, used to hear nothing at all: the application waited
|
|
1242
|
+
// for an end that could not come, until a timeout of its own. Each is
|
|
1243
|
+
// ended the way a cancelled stream is — releaseHttpStream, aborted:
|
|
1244
|
+
// 'aborted' on the request and 'close' on the response, a tunnel
|
|
1245
|
+
// destroyed, a session closed with its streams. Requests that had
|
|
1246
|
+
// finished in both directions are already gone (releaseIfDone).
|
|
1247
|
+
conns.add(conn);
|
|
1248
|
+
// Over for the application ('closing'), or its draining done ('close').
|
|
1249
|
+
var gone = function () { if (conns.delete(conn)) connGone(); };
|
|
1250
|
+
quic.on('closing', gone);
|
|
1251
|
+
quic.on('close', function () {
|
|
1252
|
+
gone();
|
|
1253
|
+
for (var hid in conn.headerDeadlines) clearHeadersDeadline(conn, hid);
|
|
1254
|
+
for (var sid in conn.http_streams) releaseHttpStream(conn, sid, true);
|
|
1255
|
+
for (var wid in conn._wtStreamObjects) releaseHttpStream(conn, wid, true);
|
|
1256
|
+
});
|
|
291
1257
|
|
|
292
|
-
//
|
|
1258
|
+
// Registered HERE, not after the handshake completes.
|
|
1259
|
+
//
|
|
1260
|
+
// The peer's QUIC transport parameters arrive inside its ClientHello,
|
|
1261
|
+
// which is processed long before 'connect' fires — a listener attached
|
|
1262
|
+
// afterwards has already missed them, and the transport section of the
|
|
1263
|
+
// fingerprint comes out empty. The HTTP/3 SETTINGS arrive later still, on
|
|
1264
|
+
// the control stream, so observeH3 is given the QUIC connection now and
|
|
1265
|
+
// the H3 connection as soon as it exists.
|
|
1266
|
+
var h3Observer = observeH3(null, quic);
|
|
1267
|
+
conn.getH3Fingerprint = function () { return h3Observer(); };
|
|
1268
|
+
|
|
1269
|
+
/**
|
|
1270
|
+
* The same handle h2_server builds, so that one piece of rate-limiting
|
|
1271
|
+
* code works against both protocols without branching on version.
|
|
1272
|
+
*/
|
|
1273
|
+
var connHandle = {
|
|
1274
|
+
httpVersion: '3.0',
|
|
1275
|
+
// quic_server attaches the address to the connection, but the listener
|
|
1276
|
+
// is handed it directly too — `peer` — and that one does not depend on
|
|
1277
|
+
// which version of quic_server is installed. Without the fallback an
|
|
1278
|
+
// older quic_server gives null here, and an IP screen that reads it
|
|
1279
|
+
// skips the connection silently rather than failing.
|
|
1280
|
+
get remoteAddress() { return quic.remoteAddress || (peer && peer.address) || null; },
|
|
1281
|
+
get remotePort() { return quic.remotePort || (peer && peer.port) || null; },
|
|
1282
|
+
|
|
1283
|
+
// Read by anything that checks before it acts, the way checkIpBlacklist
|
|
1284
|
+
// does with a TCP socket: its answer arrives asynchronously, and by then
|
|
1285
|
+
// the peer may already have gone.
|
|
1286
|
+
get destroyed() {
|
|
1287
|
+
var st = quic.state;
|
|
1288
|
+
return st === 'closed' || st === 'draining' || st === 'closing';
|
|
1289
|
+
},
|
|
1290
|
+
encrypted: true,
|
|
1291
|
+
fingerprint: function () { return h3Observer(); },
|
|
1292
|
+
getTicketInfo: function () { return quic.getTicketInfo ? quic.getTicketInfo() : null; },
|
|
1293
|
+
isSessionReused: function () { return !!(quic.isSessionReused && quic.isSessionReused()); },
|
|
1294
|
+
// This connection's options while it runs: { startupCap, reorderWindow,
|
|
1295
|
+
// pmtuDiscovery, maxUdpPayloadSize, initialRtt, initialCongestionWindow,
|
|
1296
|
+
// carefulResume } (quic_connection's setOptions).
|
|
1297
|
+
setOptions: function (o) { quic.setOptions(o); return connHandle; },
|
|
1298
|
+
getOptions: function () { return quic.getOptions(); },
|
|
1299
|
+
|
|
1300
|
+
// Refuse or cancel a stream: RESET_STREAM and STOP_SENDING both ways,
|
|
1301
|
+
// as RFC 9114 §4.1.1 describes. The intents are h2_server's — 'refused'
|
|
1302
|
+
// for a request never processed, which the client may safely retry
|
|
1303
|
+
// (H3_REQUEST_REJECTED), 'cancelled' for one abandoned part way
|
|
1304
|
+
// (H3_REQUEST_CANCELLED); a number is passed through as is.
|
|
1305
|
+
//
|
|
1306
|
+
// It used to answer 429 instead: QUIC could not reset a stream until
|
|
1307
|
+
// control frames became reliable. A 429 still costs a response, and
|
|
1308
|
+
// leaves the stream open until the client reads it.
|
|
1309
|
+
sendRstStream: function (streamId, code) {
|
|
1310
|
+
var h3code = typeof code === 'number' ? code
|
|
1311
|
+
: code === 'cancelled' ? H3_ERR.H3_REQUEST_CANCELLED
|
|
1312
|
+
: H3_ERR.H3_REQUEST_REJECTED;
|
|
1313
|
+
if (conn.h3) conn.h3.abortStream(streamId, h3code);
|
|
1314
|
+
clearHeadersDeadline(conn, streamId);
|
|
1315
|
+
releaseHttpStream(conn, streamId, true);
|
|
1316
|
+
},
|
|
1317
|
+
|
|
1318
|
+
// Stop new requests; the ones in flight finish — as h2_server's
|
|
1319
|
+
// goaway(), whose semantics a rate limiter is written against. This
|
|
1320
|
+
// closed the connection on the spot, so the same limiter code that let
|
|
1321
|
+
// HTTP/2 requests in progress complete cut HTTP/3 ones off mid-response.
|
|
1322
|
+
//
|
|
1323
|
+
// The GOAWAY carries the first request stream id NOT processed: one past
|
|
1324
|
+
// the highest the client has opened, so every request already started —
|
|
1325
|
+
// even one whose HEADERS are still arriving — is answered. When they
|
|
1326
|
+
// have all finished and been delivered, the connection is closed:
|
|
1327
|
+
// H3_NO_ERROR, or H3_EXCESSIVE_LOAD for goaway('overloaded') (HTTP/2's
|
|
1328
|
+
// ENHANCE_YOUR_CALM). Before the handshake there is no HTTP/3 to speak
|
|
1329
|
+
// of: refused, as destroy() does.
|
|
1330
|
+
goaway: function (code) {
|
|
1331
|
+
if (conn.goaway) return;
|
|
1332
|
+
if (!conn.h3) { try { quic.close(0x02, 'refused', false); } catch (e) {} return; }
|
|
1333
|
+
conn.goaway = { id: (quic.context.peer_max_opened_bidi + 1) * 4, code: h3ConnCode(code), closing: false };
|
|
1334
|
+
conn.h3.sendGoaway(conn.goaway.id);
|
|
1335
|
+
closeAfterGoaway(conn);
|
|
1336
|
+
},
|
|
1337
|
+
|
|
1338
|
+
destroy: function (code) {
|
|
1339
|
+
// Two different closes, because the right one depends on WHEN.
|
|
1340
|
+
//
|
|
1341
|
+
// Before the handshake completes there is no HTTP/3 yet — an
|
|
1342
|
+
// application error code has no meaning to the peer, and RFC 9000
|
|
1343
|
+
// §10.2.3 forbids sending one in the Initial or Handshake packet
|
|
1344
|
+
// spaces. CONNECTION_REFUSED (0x02) is the transport error that says
|
|
1345
|
+
// exactly what happened: the server declined the connection. That is
|
|
1346
|
+
// the case for an IP screen, which runs on the very first packet.
|
|
1347
|
+
//
|
|
1348
|
+
// After it, the application space, with the intent as for goaway():
|
|
1349
|
+
// H3_NO_ERROR, H3_EXCESSIVE_LOAD for 'overloaded', or a number. It sent
|
|
1350
|
+
// 0x10c — H3_REQUEST_CANCELLED, a stream's code, which says nothing
|
|
1351
|
+
// about why a connection ended.
|
|
1352
|
+
try {
|
|
1353
|
+
if (!conn.h3) quic.close(0x02, 'refused', false);
|
|
1354
|
+
else quic.close(h3ConnCode(code), 'closed', true);
|
|
1355
|
+
} catch (e) {}
|
|
1356
|
+
}
|
|
1357
|
+
};
|
|
1358
|
+
conn.handle = connHandle;
|
|
1359
|
+
|
|
1360
|
+
// Announced upward, before the handshake — the QUIC counterpart of
|
|
1361
|
+
// tcp_listener's 'connection', and there for the same reason.
|
|
1362
|
+
//
|
|
1363
|
+
// It is the one point where a server can refuse a peer before any crypto
|
|
1364
|
+
// is done on its behalf. It fires on the first Initial packet, so a
|
|
1365
|
+
// listener that calls destroy() here stops the connection before the TLS
|
|
1366
|
+
// handshake, just as destroying a TCP socket in 'connection' does.
|
|
1367
|
+
//
|
|
1368
|
+
// Without it, nothing above this file learns that a QUIC connection
|
|
1369
|
+
// exists, and screening that is wired to 'connection' — an IP blacklist,
|
|
1370
|
+
// a rate limit on new connections — applies to HTTP/1.1 and HTTP/2 and
|
|
1371
|
+
// silently not to HTTP/3.
|
|
1372
|
+
//
|
|
1373
|
+
// The handle carries remoteAddress, destroyed and destroy(), which is what
|
|
1374
|
+
// such a listener uses on a TCP socket, so the same function serves both.
|
|
1375
|
+
ev.emit('connection', connHandle);
|
|
1376
|
+
|
|
1377
|
+
// A handshake that failed — a TLS alert (a client certificate refused, no
|
|
1378
|
+
// common cipher, a size limit) or the handshake timeout: node:tls's
|
|
1379
|
+
// 'tlsClientError', with the handle in place of the socket. The QUIC
|
|
1380
|
+
// connection reports 'error' for exactly these and nothing else; it was
|
|
1381
|
+
// not listened to, so a failed handshake left no trace.
|
|
1382
|
+
//
|
|
1383
|
+
// Also when it is the client that ended the handshake with an alert (a
|
|
1384
|
+
// browser that does not trust the certificate): in QUIC that arrives as
|
|
1385
|
+
// CONNECTION_CLOSE with CRYPTO_ERROR, 0x100 + the alert (RFC 9001 §4.8),
|
|
1386
|
+
// where node:tls would report the alert itself — even after 'connect',
|
|
1387
|
+
// which a server reaches with its own Finished, before the client has
|
|
1388
|
+
// checked anything. A CRYPTO_ERROR can only come from TLS, and TLS in
|
|
1389
|
+
// QUIC is the handshake. Once per connection.
|
|
1390
|
+
var tlsErrorReported = false;
|
|
1391
|
+
function reportTlsError(err) {
|
|
1392
|
+
if (tlsErrorReported) return;
|
|
1393
|
+
tlsErrorReported = true;
|
|
1394
|
+
ev.emit('tlsClientError', err, connHandle);
|
|
1395
|
+
}
|
|
1396
|
+
quic.on('error', reportTlsError);
|
|
1397
|
+
quic.on('close', function () {
|
|
1398
|
+
var pc = quic.context && quic.context.peer_close;
|
|
1399
|
+
// The transport's CONNECTION_CLOSE only (0x1c): in the application's
|
|
1400
|
+
// (0x1d) the same numbers are HTTP/3's — 0x100 is H3_NO_ERROR, a
|
|
1401
|
+
// client closing a connection it is done with. Reported as TLS alert 0
|
|
1402
|
+
// since HTTP/3 clients began to say goodbye (h3_client's
|
|
1403
|
+
// CONNECTION_CLOSE used to be lost with its socket).
|
|
1404
|
+
if (pc && !pc.application && pc.error >= 0x100 && pc.error < 0x200) {
|
|
1405
|
+
var err = new Error('the client ended the handshake: TLS alert ' + (pc.error - 0x100) + (pc.reason ? ' (' + pc.reason + ')' : ''));
|
|
1406
|
+
err.code = 'ERR_TLS_ALERT_RECEIVED';
|
|
1407
|
+
err.alert = pc.error - 0x100;
|
|
1408
|
+
reportTlsError(err);
|
|
1409
|
+
}
|
|
1410
|
+
});
|
|
1411
|
+
|
|
1412
|
+
// 'secureConnection': the handshake is complete — the client's Finished
|
|
1413
|
+
// verified, the name and the moment node:tls uses (it was emitted when
|
|
1414
|
+
// the server's own Finished went, a round trip early, before the client
|
|
1415
|
+
// had proven anything). 'connection' fired before any TLS, knowing only
|
|
1416
|
+
// an address; here the connection can be asked what the handshake
|
|
1417
|
+
// decided (isSessionReused(), getTicketInfo()), which is what a caller
|
|
1418
|
+
// needs to tell a returning client from a new one. Before any request:
|
|
1419
|
+
// the client's are held until now. A listener may destroy() it. The same
|
|
1420
|
+
// handle as 'connection' carries — over QUIC there is no socket to hand
|
|
1421
|
+
// out.
|
|
1422
|
+
quic.on('handshake_ended', function (confirmed) {
|
|
1423
|
+
if (confirmed) ev.emit('secureConnection', connHandle);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
// Our side of the handshake done (our Finished sent): HTTP/3 set up —
|
|
1427
|
+
// its SETTINGS go with the handshake's last datagram.
|
|
293
1428
|
quic.on('connect', function () {
|
|
294
|
-
|
|
1429
|
+
if (connHandle.destroyed) return;
|
|
1430
|
+
|
|
1431
|
+
// The handshake may have settled on something other than HTTP/3 when
|
|
1432
|
+
// the server accepts more than one ALPN name. Such a connection is not
|
|
1433
|
+
// ours to speak: the application only asked that the handshake agree to
|
|
1434
|
+
// the name. Close it instead of treating its streams as HTTP/3.
|
|
1435
|
+
var negotiated = quic.alpnProtocol;
|
|
1436
|
+
if (negotiated && negotiated !== 'h3') {
|
|
1437
|
+
try { quic.close(0, 'alpn ' + negotiated + ' is not served here'); } catch (e) {}
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
var h3 = new H3Connection({
|
|
1442
|
+
quicConnection: quic,
|
|
1443
|
+
enableWebTransport: true,
|
|
1444
|
+
maxWebTransportSessions: options.maxWebTransportSessions,
|
|
1445
|
+
// Report-only mode for the checks added with the error layer. See the
|
|
1446
|
+
// note in h3.js: these turn conditions that were previously ignored
|
|
1447
|
+
// into connection errors, so a bug they expose looks like a regression.
|
|
1448
|
+
qpackLenient: options.qpackLenient === true
|
|
1449
|
+
});
|
|
295
1450
|
conn.h3 = h3;
|
|
1451
|
+
// The H3 half of the observer, now that the connection exists. The QUIC
|
|
1452
|
+
// half was attached before the handshake, above.
|
|
1453
|
+
h3Observer = observeH3(h3, quic, h3Observer.state);
|
|
1454
|
+
|
|
1455
|
+
// Surface HTTP/3-level failures on the server. Without this the error
|
|
1456
|
+
// raised by connectionError() is emitted into a connection object nobody
|
|
1457
|
+
// listens to, and the peer's ERR_QUIC_PROTOCOL_ERROR is the only sign
|
|
1458
|
+
// anything happened — invisible on this side, which is the exact failure
|
|
1459
|
+
// mode the error layer exists to remove.
|
|
1460
|
+
h3.on('error', function (err) { ev.emit('error', err); });
|
|
296
1461
|
|
|
297
1462
|
// HTTP request headers received → set_http_stream (reactive)
|
|
298
1463
|
h3.on('http_headers', function (streamId, headers) {
|
|
1464
|
+
clearHeadersDeadline(conn, streamId);
|
|
299
1465
|
set_http_stream(conn, streamId, { request_headers: headers });
|
|
300
1466
|
});
|
|
301
1467
|
|
|
1468
|
+
// The request's trailers (after its body, before its end): on req, as
|
|
1469
|
+
// node:http's req.trailers — read once 'end' has fired.
|
|
1470
|
+
h3.on('http_trailers', function (streamId, trailers) {
|
|
1471
|
+
var st = conn.http_streams[streamId];
|
|
1472
|
+
if (st && st.req) st.req.trailers = trailers;
|
|
1473
|
+
});
|
|
1474
|
+
|
|
302
1475
|
// HTTP request body chunk → set_http_stream
|
|
303
1476
|
h3.on('http_body', function (streamId, data) {
|
|
304
1477
|
set_http_stream(conn, streamId, { request_body_chunk: data });
|
|
@@ -309,19 +1482,120 @@ function createServer(options, handler) {
|
|
|
309
1482
|
set_http_stream(conn, streamId, { request_end: true });
|
|
310
1483
|
});
|
|
311
1484
|
|
|
1485
|
+
// Bytes on an accepted extended-CONNECT tunnel: WebSocket frames, not
|
|
1486
|
+
// HTTP/3 frames. They go straight to the Duplex the upgrade handler was
|
|
1487
|
+
// given.
|
|
1488
|
+
h3.on('stream_open', function (streamId) {
|
|
1489
|
+
armHeadersDeadline(conn, streamId);
|
|
1490
|
+
ev.emit('stream_open', connHandle, streamId);
|
|
1491
|
+
});
|
|
1492
|
+
// The client cancelled a stream. Same event and arguments as
|
|
1493
|
+
// h2_server's 'stream_reset', so a Rapid Reset detector is written once
|
|
1494
|
+
// for both protocols.
|
|
1495
|
+
//
|
|
1496
|
+
// HTTP/2 has one frame for it (RST_STREAM); HTTP/3 has two, one per
|
|
1497
|
+
// direction, and which ones arrive depends on the request. A browser
|
|
1498
|
+
// cancelling a GET sends only STOP_SENDING — its side of the stream is
|
|
1499
|
+
// already finished, so there is nothing to reset — while cancelling an
|
|
1500
|
+
// upload sends both. Reporting only RESET_STREAM made every cancelled
|
|
1501
|
+
// GET invisible, and GET-then-cancel is exactly what a Rapid Reset
|
|
1502
|
+
// attack sends. So: either frame reports the cancellation, once per
|
|
1503
|
+
// stream.
|
|
1504
|
+
var cancelReported = new Set();
|
|
1505
|
+
// Rapid Reset: past streamResetBurst / streamResetRate cancelled
|
|
1506
|
+
// request streams, the connection is closed with H3_EXCESSIVE_LOAD.
|
|
1507
|
+
var resetTooMany = streamResetLimiter(options);
|
|
1508
|
+
function reportCancel(streamId, code, kind) {
|
|
1509
|
+
clearHeadersDeadline(conn, streamId);
|
|
1510
|
+
// A WebTransport stream: the direction the peer gave up on ends, with
|
|
1511
|
+
// the application's code (wt_stream's _peerCancelled); the stream
|
|
1512
|
+
// object decides what that leaves of it, and is forgotten when it
|
|
1513
|
+
// closes. Its two directions are independent — destroying it here
|
|
1514
|
+
// reset the one the peer had not touched.
|
|
1515
|
+
var wtCancelled = conn._wtStreamObjects[streamId];
|
|
1516
|
+
var isWtStream = !!(wtCancelled && wtCancelled._peerCancelled);
|
|
1517
|
+
if (isWtStream) {
|
|
1518
|
+
wtCancelled._peerCancelled(kind, code);
|
|
1519
|
+
} else {
|
|
1520
|
+
var hadRequest = streamId in conn.http_streams;
|
|
1521
|
+
var cancelled = conn.http_streams[streamId];
|
|
1522
|
+
var sendingOpen = !!cancelled && !(cancelled.res && cancelled.res._finished);
|
|
1523
|
+
releaseHttpStream(conn, streamId, true);
|
|
1524
|
+
// The client reset its request (RESET_STREAM alone — an upload
|
|
1525
|
+
// abandoned): the request is aborted above, and with it whatever
|
|
1526
|
+
// response was still to come (a tunnel, a WebTransport session, a
|
|
1527
|
+
// response not yet ended). Our sending part is reset with it
|
|
1528
|
+
// (H3_REQUEST_CANCELLED): left open, nothing would ever end it —
|
|
1529
|
+
// the stream never closed, and the client's stream credit with it.
|
|
1530
|
+
// A response already ended goes on to arrive.
|
|
1531
|
+
if (kind === 'stream_reset' && sendingOpen && conn.quic && conn.quic.resetStream) {
|
|
1532
|
+
try { conn.quic.resetStream(Number(streamId), H3_ERR.H3_REQUEST_CANCELLED); } catch (e) {}
|
|
1533
|
+
}
|
|
1534
|
+
// The client will not read the response (STOP_SENDING): the request
|
|
1535
|
+
// is cancelled, so stop reading its stream too (RFC 9114 §4.1.1).
|
|
1536
|
+
// Body or trailers still parsed on it would otherwise reach
|
|
1537
|
+
// set_http_stream for a request no longer there. Our sending part
|
|
1538
|
+
// was already reset by the transport, in answer to the STOP_SENDING.
|
|
1539
|
+
//
|
|
1540
|
+
// Not for RESET_STREAM: nothing more is read after it anyway, and
|
|
1541
|
+
// a client that only cut its request short may still want the
|
|
1542
|
+
// response (§4.1.1's early response) — resetting it would discard a
|
|
1543
|
+
// reply the handler already wrote.
|
|
1544
|
+
if (kind === 'stop_sending' && hadRequest && conn.h3) {
|
|
1545
|
+
try { conn.h3.stopReading(streamId, H3_ERR.H3_REQUEST_CANCELLED); } catch (e) {}
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
if (cancelReported.has(streamId)) return;
|
|
1549
|
+
cancelReported.add(streamId);
|
|
1550
|
+
if (cancelReported.size > 4096) cancelReported.delete(cancelReported.values().next().value);
|
|
1551
|
+
ev.emit('stream_reset', connHandle, streamId, code);
|
|
1552
|
+
// Request streams only: a WebTransport application cancels its own
|
|
1553
|
+
// streams as a matter of course (a media frame gone stale), and those
|
|
1554
|
+
// reach no request handler.
|
|
1555
|
+
if (!isWtStream && resetTooMany()) {
|
|
1556
|
+
try { quic.close(H3_ERR.H3_EXCESSIVE_LOAD, 'too many stream resets', true); } catch (e) {}
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
h3.on('stream_reset', function (sid, code) { reportCancel(sid, code, 'stream_reset'); });
|
|
1560
|
+
h3.on('stop_sending', function (sid, code) { reportCancel(sid, code, 'stop_sending'); });
|
|
1561
|
+
|
|
1562
|
+
// We ended a stream over what the peer sent on it (h3 streamError):
|
|
1563
|
+
// whatever request was built for it is aborted, as for a cancel.
|
|
1564
|
+
h3.on('stream_error', function (streamId) {
|
|
1565
|
+
clearHeadersDeadline(conn, streamId);
|
|
1566
|
+
releaseHttpStream(conn, streamId, true);
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
// A WebTransport session ended in the HTTP/3 layer — its CONNECT stream
|
|
1570
|
+
// finished or was reset — and its streams were reset there. Here, the
|
|
1571
|
+
// same end: the session's streams are released (destroyed).
|
|
1572
|
+
h3.on('wt_session_closed', function (sessionId) {
|
|
1573
|
+
releaseSessionStreams(conn, Number(sessionId));
|
|
1574
|
+
});
|
|
1575
|
+
|
|
1576
|
+
h3.on('tunnel_data', function (streamId, data, fin) {
|
|
1577
|
+
var stream = conn.http_streams[streamId];
|
|
1578
|
+
if (!stream || !stream.tunnel) return;
|
|
1579
|
+
if (data && data.byteLength) {
|
|
1580
|
+
stream.tunnel.push(Buffer.from(data.buffer, data.byteOffset, data.byteLength));
|
|
1581
|
+
}
|
|
1582
|
+
if (fin) {
|
|
1583
|
+
stream.tunnel.push(null);
|
|
1584
|
+
releaseHttpStream(conn, streamId, false);
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
|
|
312
1588
|
// ---- WebTransport stream events ----
|
|
313
1589
|
|
|
314
1590
|
// New WT stream (bidi or uni from client)
|
|
315
1591
|
h3.on('wt_stream', function (sessionId, wtStreamId, data, fin, isBidi) {
|
|
316
1592
|
var session = conn.http_streams[sessionId];
|
|
317
|
-
|
|
1593
|
+
// No session to give it to: nothing will read it, so nothing holds
|
|
1594
|
+
// its credit either.
|
|
1595
|
+
if (!session || !session.req._isWebTransport) { quic.releaseStreamCredit(wtStreamId); return; }
|
|
318
1596
|
|
|
319
1597
|
// Create a Duplex-like stream for the WT stream
|
|
320
|
-
var wtStream = _createWtStream(conn, quic, wtStreamId, sessionId, isBidi);
|
|
321
|
-
|
|
322
|
-
// Push initial data
|
|
323
|
-
if (data.byteLength > 0) _emitOnReq(session.req, 'data:' + wtStreamId, data);
|
|
324
|
-
if (fin) _emitOnReq(session.req, 'end:' + wtStreamId);
|
|
1598
|
+
var wtStream = _createWtStream(conn, quic, wtStreamId, sessionId, isBidi ? 'bidi' : 'uni-in');
|
|
325
1599
|
|
|
326
1600
|
// Emit on req
|
|
327
1601
|
if (isBidi) {
|
|
@@ -332,34 +1606,44 @@ function createServer(options, handler) {
|
|
|
332
1606
|
|
|
333
1607
|
// If initial data, push to stream
|
|
334
1608
|
if (data.byteLength > 0) wtStream._pushData(data);
|
|
335
|
-
|
|
1609
|
+
// Released at FIN, as 'wt_data' below does — a stream whose whole
|
|
1610
|
+
// content came in its first delivery never reaches that handler.
|
|
1611
|
+
if (fin) { wtStream._endData(); releaseHttpStream(conn, wtStreamId, false); }
|
|
336
1612
|
});
|
|
337
1613
|
|
|
338
1614
|
// Subsequent data on WT stream
|
|
339
1615
|
h3.on('wt_data', function (sessionId, wtStreamId, data, fin) {
|
|
340
|
-
if (!conn._wtStreamObjects) return;
|
|
341
1616
|
var wtStream = conn._wtStreamObjects[wtStreamId];
|
|
342
1617
|
if (!wtStream) return;
|
|
343
1618
|
if (data.byteLength > 0) wtStream._pushData(data);
|
|
344
|
-
if (fin) { wtStream._endData();
|
|
1619
|
+
if (fin) { wtStream._endData(); releaseHttpStream(conn, wtStreamId, false); }
|
|
345
1620
|
});
|
|
346
1621
|
|
|
347
|
-
// Datagrams
|
|
1622
|
+
// Datagrams: to the session their Quarter Stream ID names (RFC 9297
|
|
1623
|
+
// §2.1), looked up — not searched for among every request on the
|
|
1624
|
+
// connection, once per datagram.
|
|
348
1625
|
quic.on('datagram', function (rawData) {
|
|
349
1626
|
if (!rawData || rawData.byteLength === 0) return;
|
|
350
1627
|
var result = readVarInt(rawData, 0);
|
|
351
1628
|
if (!result) return;
|
|
352
|
-
|
|
353
|
-
var
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
if (session.req._isWebTransport && Math.floor(Number(sid) / 4) === quarterSid) {
|
|
359
|
-
_emitOnReq(session.req, 'datagram', Buffer.from(payload));
|
|
360
|
-
break;
|
|
361
|
-
}
|
|
1629
|
+
// A CONNECT-UDP tunnel's: the rest is Context ID and payload.
|
|
1630
|
+
var udpSid = conn.udpByQuarter[result.value];
|
|
1631
|
+
if (udpSid !== undefined) {
|
|
1632
|
+
var ust = conn.http_streams[udpSid];
|
|
1633
|
+
if (ust && ust.udp && ust.udp.sink) ust.udp.sink.datagram(rawData.subarray(result.byteLength));
|
|
1634
|
+
return;
|
|
362
1635
|
}
|
|
1636
|
+
var session = conn.http_streams[conn.wtByQuarter[result.value]];
|
|
1637
|
+
if (!session || !session.req._isWebTransport) return;
|
|
1638
|
+
// One copy, into a buffer of its own: exact size, byteOffset 0.
|
|
1639
|
+
// This was slice() then Buffer.from() — two copies, and a
|
|
1640
|
+
// Buffer.from() under 4KB is carved out of Node's shared 8KB
|
|
1641
|
+
// pool: a consumer that hands the datagram to another thread
|
|
1642
|
+
// (postMessage) then clones the whole 8KB pool for every one.
|
|
1643
|
+
var payload = new Uint8Array(rawData.byteLength - result.byteLength);
|
|
1644
|
+
payload.set(rawData.subarray(result.byteLength));
|
|
1645
|
+
if (session.req._wtDatagrams) session.req._wtDatagrams._push(payload);
|
|
1646
|
+
_emitOnReq(session.req, 'datagram', Buffer.from(payload.buffer));
|
|
363
1647
|
});
|
|
364
1648
|
});
|
|
365
1649
|
});
|
|
@@ -372,10 +1656,62 @@ function createServer(options, handler) {
|
|
|
372
1656
|
// server. This layer adds HTTP/3 request events on top.
|
|
373
1657
|
// ============================================================
|
|
374
1658
|
|
|
1659
|
+
// ---- Closing, as node:http (see h1_server) ----
|
|
1660
|
+
//
|
|
1661
|
+
// Idle: no request open on it — before its handshake too, as node:http
|
|
1662
|
+
// counts a connection that has not sent a request. GOAWAY, and closed at
|
|
1663
|
+
// once (closeAfterGoaway: nothing to wait for); before the handshake,
|
|
1664
|
+
// refused (goaway() does that).
|
|
1665
|
+
function isIdle(conn) { return Object.keys(conn.http_streams).length === 0; }
|
|
1666
|
+
// GOAWAY and CONNECTION_CLOSE together (§5.2 allows closing at once when
|
|
1667
|
+
// nothing is in flight): waiting for the GOAWAY's acknowledgement, a
|
|
1668
|
+
// client that had gone held the close until its idle timeout.
|
|
1669
|
+
function closeIdle(conn) {
|
|
1670
|
+
if (!conn.handle) return;
|
|
1671
|
+
conn.handle.goaway();
|
|
1672
|
+
conn.handle.destroy();
|
|
1673
|
+
}
|
|
1674
|
+
function closeIdleConnections() {
|
|
1675
|
+
for (var conn of conns) if (isIdle(conn)) closeIdle(conn);
|
|
1676
|
+
}
|
|
1677
|
+
function closeAllConnections() {
|
|
1678
|
+
for (var conn of conns) if (conn.handle) conn.handle.destroy();
|
|
1679
|
+
}
|
|
1680
|
+
// No new connections; GOAWAY on each — the requests in flight finish, then
|
|
1681
|
+
// the connection closes (closeAfterGoaway) — and once none is left, the
|
|
1682
|
+
// UDP sockets. It closed every connection on the spot: a response being
|
|
1683
|
+
// sent was cut off, and its client waited for the rest until a timeout.
|
|
1684
|
+
function close(callback) {
|
|
1685
|
+
qserver.stopAccepting();
|
|
1686
|
+
closingServer = typeof callback === 'function' ? callback : function () {};
|
|
1687
|
+
for (var conn of conns) {
|
|
1688
|
+
if (isIdle(conn)) closeIdle(conn);
|
|
1689
|
+
else if (conn.handle) conn.handle.goaway();
|
|
1690
|
+
}
|
|
1691
|
+
connGone();
|
|
1692
|
+
}
|
|
1693
|
+
function connGone() {
|
|
1694
|
+
if (!closingServer || conns.size > 0) return;
|
|
1695
|
+
var cb = closingServer;
|
|
1696
|
+
closingServer = null;
|
|
1697
|
+
qserver.close(cb);
|
|
1698
|
+
}
|
|
1699
|
+
function setLimits(o) {
|
|
1700
|
+
if (o.headersTimeout !== undefined) headersTimeout = o.headersTimeout;
|
|
1701
|
+
if (o.requestTimeout !== undefined) requestTimeout = o.requestTimeout;
|
|
1702
|
+
if (o.maxRequestsPerSocket !== undefined) maxRequests = maxRequestsPerSocket(o.maxRequestsPerSocket);
|
|
1703
|
+
}
|
|
1704
|
+
|
|
375
1705
|
return {
|
|
376
|
-
listen: qserver.listen,
|
|
377
|
-
close:
|
|
378
|
-
|
|
1706
|
+
listen: function (p, h, cb) { closingServer = null; return qserver.listen(p, h, cb); },
|
|
1707
|
+
close: close,
|
|
1708
|
+
closeIdleConnections: closeIdleConnections,
|
|
1709
|
+
closeAllConnections: closeAllConnections,
|
|
1710
|
+
setLimits: setLimits,
|
|
1711
|
+
get connectionCount() { return conns.size; },
|
|
1712
|
+
ref: qserver.ref,
|
|
1713
|
+
unref: qserver.unref,
|
|
1714
|
+
on: function (name, fn) { if (name === 'upgrade') upgradeListeners++; ev.on(name, fn); },
|
|
379
1715
|
setTimeout: function () { /* TODO */ },
|
|
380
1716
|
|
|
381
1717
|
/** Feed an incoming UDP packet when running in external-socket mode.
|
|
@@ -384,7 +1720,21 @@ function createServer(options, handler) {
|
|
|
384
1720
|
|
|
385
1721
|
/** Returns true if the 5-tuple matches an active QUIC connection.
|
|
386
1722
|
* Used by demuxers for routing decisions on shared UDP ports. */
|
|
387
|
-
hasConnection: qserver.hasConnection
|
|
1723
|
+
hasConnection: qserver.hasConnection,
|
|
1724
|
+
|
|
1725
|
+
getTicketKeys: qserver.getTicketKeys,
|
|
1726
|
+
setTicketKeys: qserver.setTicketKeys,
|
|
1727
|
+
getEchKeys: qserver.getEchKeys,
|
|
1728
|
+
setEchKeys: qserver.setEchKeys,
|
|
1729
|
+
getValidateAddress: qserver.getValidateAddress,
|
|
1730
|
+
setValidateAddress: qserver.setValidateAddress,
|
|
1731
|
+
getPendingHandshakes: qserver.getPendingHandshakes,
|
|
1732
|
+
getOptions: qserver.getOptions,
|
|
1733
|
+
setOptions: qserver.setOptions,
|
|
1734
|
+
getResetTokenSecret: qserver.getResetTokenSecret,
|
|
1735
|
+
setResetTokenSecret: qserver.setResetTokenSecret,
|
|
1736
|
+
getTokenSecret: qserver.getTokenSecret,
|
|
1737
|
+
setTokenSecret: qserver.setTokenSecret
|
|
388
1738
|
};
|
|
389
1739
|
}
|
|
390
1740
|
|