quico 0.4.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1768 -155
- package/index.js +55 -36
- package/package.json +3 -3
- package/src/agent.js +306 -36
- package/src/client.js +447 -120
- package/src/client_profile.js +266 -0
- package/src/connect_udp.js +578 -0
- package/src/connect_udp_client.js +770 -0
- package/src/crypto.js +240 -140
- package/src/h1.js +953 -0
- package/src/h1_client.js +731 -0
- package/src/h1_server.js +624 -0
- package/src/h2.js +1325 -0
- package/src/h2_client.js +675 -0
- package/src/h2_fingerprint.js +403 -0
- package/src/h2_frames.js +582 -0
- package/src/h2_server.js +862 -0
- package/src/h3.js +1966 -469
- package/src/h3_client.js +467 -48
- package/src/h3_server.js +1349 -94
- package/src/hpack.js +455 -0
- package/src/hpack_primitives.js +520 -0
- package/src/http_common.js +901 -0
- package/src/ja4h.js +256 -0
- package/src/qpack.js +555 -0
- package/src/quic_connection.js +6517 -657
- package/src/quic_server.js +768 -73
- package/src/quic_socket.js +134 -9
- package/src/quic_version.js +143 -0
- package/src/server.js +843 -115
- package/src/server_stats.js +93 -0
- package/src/streams.js +361 -35
- package/src/tcp_listener.js +557 -0
- package/src/tls_bridge.js +474 -73
- package/src/tls_options.js +108 -0
- package/src/transport.js +455 -46
- package/src/utils.js +94 -12
- package/src/webtransport.js +433 -436
- package/src/ws.js +624 -0
- package/src/ws_client.js +641 -0
- package/src/ws_server.js +833 -0
- package/src/wt_stream.js +618 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// The server's counters — server.getStats()
|
|
3
|
+
// ============================================================
|
|
4
|
+
//
|
|
5
|
+
// What an operator watches a server by: how many connections came, over what,
|
|
6
|
+
// how many handshakes failed, how connections ended and with what code, what
|
|
7
|
+
// 0-RTT did, how often the anti-amplification limit held the server back,
|
|
8
|
+
// how many internal errors were caught. Counts only: no addresses, no names,
|
|
9
|
+
// no headers, no payloads — nothing here identifies a client.
|
|
10
|
+
//
|
|
11
|
+
// One object per server, made by createServer and handed to each layer
|
|
12
|
+
// (quic_server, quic_connection, h3_server, tcp_listener, h2_server / h2.js,
|
|
13
|
+
// h1_server) in its options as `stats`; each counts where the thing happens,
|
|
14
|
+
// with a plain increment. A layer used on its own (createQuicServer, an
|
|
15
|
+
// H2Connection) has none and counts nothing.
|
|
16
|
+
//
|
|
17
|
+
// Codes are the protocol's, as hex strings ('0x0', '0x12f'): a map from code
|
|
18
|
+
// to count, only for codes seen.
|
|
19
|
+
|
|
20
|
+
function ServerStats() {
|
|
21
|
+
this.since = Date.now();
|
|
22
|
+
// Connections taken: QUIC, a first Initial that began a handshake; TCP, a
|
|
23
|
+
// socket accepted (before TLS).
|
|
24
|
+
this.connections = { quic: 0, tcp: 0 };
|
|
25
|
+
// TLS handshakes, as they ended: completed (the client's Finished
|
|
26
|
+
// verified) or failed (an alert either way, a timeout, the connection
|
|
27
|
+
// closed first). resumed: completed with a session ticket.
|
|
28
|
+
this.handshakes = { quic: { completed: 0, failed: 0 }, tcp: { completed: 0, failed: 0 }, resumed: 0 };
|
|
29
|
+
// Connections by the protocol they went on to speak (the ALPN agreed; over
|
|
30
|
+
// cleartext TCP, the preface read) — other: an ALPN name of the
|
|
31
|
+
// application's.
|
|
32
|
+
this.protocols = { h3: 0, h2: 0, 'http/1.1': 0, other: 0 };
|
|
33
|
+
// 0-RTT offered by the client: taken, or refused (the ticket's, the replay
|
|
34
|
+
// check, a HelloRetryRequest); tooEarly, requests answered 425 (RFC 8470).
|
|
35
|
+
this.earlyData = { accepted: 0, rejected: 0, tooEarly: 0 };
|
|
36
|
+
// How connections ended, by code:
|
|
37
|
+
// quic CONNECTION_CLOSE of the transport (RFC 9000 §20.1; 0x100 + a TLS
|
|
38
|
+
// alert, RFC 9001 §4.8) — sent by us, received from the client;
|
|
39
|
+
// silent: ended with none either way (the idle timeout, a
|
|
40
|
+
// stateless reset, a handshake abandoned)
|
|
41
|
+
// h3 CONNECTION_CLOSE of the application (RFC 9114 §8.1)
|
|
42
|
+
// h2 GOAWAY (RFC 9113 §7)
|
|
43
|
+
// http1 requests refused before the application saw them, by status
|
|
44
|
+
// (400 a malformed request, 431 headers too large, ...)
|
|
45
|
+
this.closes = {
|
|
46
|
+
quic: { sent: {}, received: {}, silent: 0 },
|
|
47
|
+
h3: { sent: {}, received: {} },
|
|
48
|
+
h2: { sent: {}, received: {} },
|
|
49
|
+
http1: { sent: {} }
|
|
50
|
+
};
|
|
51
|
+
// Streams reset, by code: RESET_STREAM (HTTP/3) and RST_STREAM (HTTP/2),
|
|
52
|
+
// sent and received.
|
|
53
|
+
this.streamResets = { h3: { sent: {}, received: {} }, h2: { sent: {}, received: {} } };
|
|
54
|
+
// Datagrams the server held because the client's address was not yet
|
|
55
|
+
// validated and it had sent three times what it received (RFC 9000 §8.1).
|
|
56
|
+
this.amplificationBlocked = 0;
|
|
57
|
+
// Exceptions caught inside the QUIC and HTTP/3 layers: a bug of ours,
|
|
58
|
+
// never the client's (the packet dropped, or H3_INTERNAL_ERROR).
|
|
59
|
+
this.internalErrors = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** One more of a protocol error `code` in `map`, by its hex. */
|
|
63
|
+
ServerStats.prototype.code = function (map, code) {
|
|
64
|
+
// Up to 62 bits (a QUIC varint — WebTransport's codes are past 2^32): by
|
|
65
|
+
// its value, not cut to 32 bits.
|
|
66
|
+
var k = '0x' + (typeof code === 'bigint' ? code : Number(code)).toString(16);
|
|
67
|
+
map[k] = (map[k] || 0) + 1;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/** One more of an HTTP status in `map`. */
|
|
71
|
+
ServerStats.prototype.status = function (map, status) {
|
|
72
|
+
map[status] = (map[status] || 0) + 1;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** A TLS handshake ended ('quic' or 'tcp'): completed or not, resumed. */
|
|
76
|
+
ServerStats.prototype.handshake = function (transport, ok, resumed) {
|
|
77
|
+
this.handshakes[transport][ok ? 'completed' : 'failed']++;
|
|
78
|
+
if (ok && resumed) this.handshakes.resumed++;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/** A connection handed to the protocol it speaks. */
|
|
82
|
+
ServerStats.prototype.protocol = function (name) {
|
|
83
|
+
this.protocols[name === 'h3' || name === 'h2' || name === 'http/1.1' ? name : 'other']++;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/** A copy, for the caller to keep or change. */
|
|
87
|
+
ServerStats.prototype.snapshot = function () {
|
|
88
|
+
var copy = JSON.parse(JSON.stringify(this));
|
|
89
|
+
copy.uptime = Date.now() - this.since;
|
|
90
|
+
return copy;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export { ServerStats };
|
package/src/streams.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { Readable, Writable, Duplex } from 'node:stream';
|
|
16
|
+
import { validateHeaderName, validateHeaderValue, validateStatus, validateRequestTarget, headersSentError, responsePriority } from './http_common.js';
|
|
16
17
|
|
|
17
18
|
|
|
18
19
|
// ============================================================
|
|
@@ -52,6 +53,8 @@ class IncomingMessage extends Readable {
|
|
|
52
53
|
// Server-side (request)
|
|
53
54
|
this.method = options.method || null;
|
|
54
55
|
this.url = options.url || null;
|
|
56
|
+
// HTTP/3: the request came in 0-RTT (it may be a replay). Never over TCP.
|
|
57
|
+
this.earlyData = false;
|
|
55
58
|
|
|
56
59
|
// QUIC has no TCP socket — stub so libraries don't crash
|
|
57
60
|
this.socket = options.socket || _makeSocketStub();
|
|
@@ -73,13 +76,15 @@ class IncomingMessage extends Readable {
|
|
|
73
76
|
/**
|
|
74
77
|
* Called by H3 layer when a body chunk arrives.
|
|
75
78
|
*/
|
|
79
|
+
// push()'s answer: false once the buffer is full — the caller stops
|
|
80
|
+
// reading until _read asks for more.
|
|
76
81
|
_pushData(chunk) {
|
|
77
|
-
if (this.complete) return;
|
|
82
|
+
if (this.complete) return true;
|
|
78
83
|
if (typeof chunk === 'string') chunk = Buffer.from(chunk);
|
|
79
84
|
if (chunk instanceof Uint8Array && !Buffer.isBuffer(chunk)) {
|
|
80
85
|
chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
81
86
|
}
|
|
82
|
-
this.push(chunk);
|
|
87
|
+
return this.push(chunk);
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -92,14 +97,31 @@ class IncomingMessage extends Readable {
|
|
|
92
97
|
}
|
|
93
98
|
|
|
94
99
|
/**
|
|
95
|
-
*
|
|
100
|
+
* The exchange ended before its body did — the peer reset the stream or
|
|
101
|
+
* went away, or (client side) req.destroy(). The one place this is decided,
|
|
102
|
+
* as node:http's IncomingMessage does it:
|
|
103
|
+
*
|
|
104
|
+
* 'aborted' — always, once;
|
|
105
|
+
* 'error' — ('aborted', ECONNRESET, or the cause given) only to a
|
|
106
|
+
* listener that asked for one: unheard, it would take the
|
|
107
|
+
* process down, and node:http teaches 'aborted'/'close';
|
|
108
|
+
* 'close' — and never 'end', which would say the body is whole.
|
|
109
|
+
*
|
|
110
|
+
* It ended the stream with push(null) — 'end', a cut-off upload read as a
|
|
111
|
+
* complete one — and each caller decided the 'error' question for itself,
|
|
112
|
+
* one (h1_server) not at all, so a client vanishing mid-upload threw.
|
|
113
|
+
* A body that had already arrived in full is left as it is: 'aborted', and
|
|
114
|
+
* nothing to cut.
|
|
96
115
|
*/
|
|
97
116
|
_abort(err) {
|
|
98
|
-
if (this.
|
|
117
|
+
if (this.aborted) return;
|
|
99
118
|
this.aborted = true;
|
|
119
|
+
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
|
|
120
|
+
this.emit('aborted');
|
|
121
|
+
if (this.complete) return;
|
|
100
122
|
this.complete = true;
|
|
101
|
-
if (err)
|
|
102
|
-
|
|
123
|
+
if (!err) { err = new Error('aborted'); err.code = 'ECONNRESET'; }
|
|
124
|
+
this.destroy(this.listenerCount('error') > 0 ? err : null);
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
setTimeout(msecs, callback) {
|
|
@@ -110,6 +132,32 @@ class IncomingMessage extends Readable {
|
|
|
110
132
|
}
|
|
111
133
|
return this;
|
|
112
134
|
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* What destroy() by the application means on the wire, given by the
|
|
138
|
+
* transport that made this message: a request's body the server no longer
|
|
139
|
+
* wants (STOP_SENDING / RST_STREAM; over HTTP/1.1, the socket), a response
|
|
140
|
+
* the client gave up on mid-way. Not run when the stream itself ended it
|
|
141
|
+
* (_abort: the peer's reset, the connection gone); each hook decides what
|
|
142
|
+
* is left to stop (a body that arrived whole: nothing). destroy() was
|
|
143
|
+
* local only: the body kept coming, at full speed once its credit was
|
|
144
|
+
* released, into a message nobody read.
|
|
145
|
+
*/
|
|
146
|
+
_onDestroy(fn) {
|
|
147
|
+
(this._destroyHooks || (this._destroyHooks = [])).push(fn);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
_destroy(err, cb) {
|
|
151
|
+
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
|
|
152
|
+
var hooks = this._destroyHooks;
|
|
153
|
+
this._destroyHooks = null;
|
|
154
|
+
// Not after 'end': that is autoDestroy closing a body read to the end,
|
|
155
|
+
// not the application giving anything up.
|
|
156
|
+
if (hooks && !this.aborted && !this.readableEnded) {
|
|
157
|
+
for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
|
|
158
|
+
}
|
|
159
|
+
cb(err);
|
|
160
|
+
}
|
|
113
161
|
}
|
|
114
162
|
|
|
115
163
|
|
|
@@ -128,7 +176,10 @@ class ServerResponse extends Writable {
|
|
|
128
176
|
options = options || {};
|
|
129
177
|
|
|
130
178
|
this.statusCode = 200;
|
|
131
|
-
|
|
179
|
+
// Unset, as node:http has it: the standard reason phrase for whatever the
|
|
180
|
+
// status ends up being. It was 'OK', so a status set without one —
|
|
181
|
+
// res.statusCode = 404, writeHead(425, headers) — went out as '404 OK'.
|
|
182
|
+
this.statusMessage = undefined;
|
|
132
183
|
this.headersSent = false;
|
|
133
184
|
this.finished = false;
|
|
134
185
|
this._headers = {};
|
|
@@ -144,9 +195,11 @@ class ServerResponse extends Writable {
|
|
|
144
195
|
this.socket = options.socket || _makeSocketStub();
|
|
145
196
|
this.connection = this.socket;
|
|
146
197
|
|
|
198
|
+
// 'close' comes from the Writable itself (autoDestroy), once, after
|
|
199
|
+
// 'finish' — node's order. It was also emitted here, from a 'finish'
|
|
200
|
+
// listener registered first: the application saw close, finish, close.
|
|
147
201
|
this.on('finish', () => {
|
|
148
202
|
this.finished = true;
|
|
149
|
-
this.emit('close');
|
|
150
203
|
});
|
|
151
204
|
|
|
152
205
|
// Express does res.__proto__ = app.response (http.ServerResponse.prototype)
|
|
@@ -164,10 +217,49 @@ class ServerResponse extends Writable {
|
|
|
164
217
|
this.end = this.end.bind(this);
|
|
165
218
|
}
|
|
166
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The peer is gone — the stream reset, the connection lost — while this
|
|
222
|
+
* response was still being written. As node:http: 'close', once; later
|
|
223
|
+
* writes are dropped without an error, so a handler mid-flight is not
|
|
224
|
+
* taken down, and a pipe() into the response stops. Without it a response
|
|
225
|
+
* whose connection died never closed, and a pipe() kept reading its source
|
|
226
|
+
* into a connection that was gone.
|
|
227
|
+
*/
|
|
228
|
+
_abortResponse() {
|
|
229
|
+
if (this.writableFinished || this.destroyed) return;
|
|
230
|
+
this._aborted = true;
|
|
231
|
+
this.destroy();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* res.destroy([err]) by the application, mid-response: the transport ends
|
|
236
|
+
* the stream so the client hears it — RESET_STREAM over HTTP/3, RST_STREAM
|
|
237
|
+
* over HTTP/2, the socket over HTTP/1.1 (as node:http) — with an internal
|
|
238
|
+
* error's code when there is an err (a failed pipeline), a cancel's
|
|
239
|
+
* otherwise. It was local only: the client waited for the rest of a
|
|
240
|
+
* response that would never come, and over HTTP/3 and HTTP/2 the stream
|
|
241
|
+
* stayed open until the connection closed. As node:http, the err is not
|
|
242
|
+
* re-emitted as 'error' to a response nobody listens to.
|
|
243
|
+
*/
|
|
244
|
+
_onDestroy(fn) {
|
|
245
|
+
(this._destroyHooks || (this._destroyHooks = [])).push(fn);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_destroy(err, cb) {
|
|
249
|
+
var hooks = this._destroyHooks;
|
|
250
|
+
this._destroyHooks = null;
|
|
251
|
+
if (hooks && !this._aborted && !this.writableFinished) {
|
|
252
|
+
for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
|
|
253
|
+
}
|
|
254
|
+
cb(err && this.listenerCount('error') > 0 ? err : null);
|
|
255
|
+
}
|
|
256
|
+
|
|
167
257
|
// ---- Header management (mirrors node:http) ----
|
|
168
258
|
|
|
169
259
|
setHeader(name, value) {
|
|
170
|
-
if (this.headersSent) throw
|
|
260
|
+
if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
|
|
261
|
+
validateHeaderName(name);
|
|
262
|
+
validateHeaderValue(name, value);
|
|
171
263
|
var lower = name.toLowerCase();
|
|
172
264
|
this._headers[lower] = value;
|
|
173
265
|
this._headerNames[lower] = name;
|
|
@@ -193,13 +285,32 @@ class ServerResponse extends Writable {
|
|
|
193
285
|
}
|
|
194
286
|
|
|
195
287
|
removeHeader(name) {
|
|
196
|
-
if (this.headersSent) throw
|
|
288
|
+
if (this.headersSent) throw headersSentError('remove', this.req && this.req.httpVersion);
|
|
197
289
|
var lower = name.toLowerCase();
|
|
198
290
|
delete this._headers[lower];
|
|
199
291
|
delete this._headerNames[lower];
|
|
200
292
|
return this;
|
|
201
293
|
}
|
|
202
294
|
|
|
295
|
+
/**
|
|
296
|
+
* node:http's res.addTrailers(headers): fields sent after the body — an
|
|
297
|
+
* object, or an array of [name, value] pairs. Over HTTP/1.1 they go in the
|
|
298
|
+
* last chunk (chunked responses only, as node); over HTTP/2 as a trailing
|
|
299
|
+
* HEADERS frame. The layers read res.trailers, but the method an application
|
|
300
|
+
* calls was missing: it threw a TypeError inside the handler, and over
|
|
301
|
+
* HTTP/2 that tore down the whole connection.
|
|
302
|
+
*/
|
|
303
|
+
addTrailers(headers) {
|
|
304
|
+
if (!this.trailers) this.trailers = {};
|
|
305
|
+
var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
|
|
306
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
307
|
+
var name = pairs[i][0], value = pairs[i][1];
|
|
308
|
+
validateHeaderName(name, 'Trailer name');
|
|
309
|
+
validateHeaderValue(name, value);
|
|
310
|
+
this.trailers[String(name).toLowerCase()] = value;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
203
314
|
// ---- writeHead ----
|
|
204
315
|
|
|
205
316
|
writeHead(statusCode, statusMessage, headers) {
|
|
@@ -210,6 +321,7 @@ class ServerResponse extends Writable {
|
|
|
210
321
|
statusMessage = undefined;
|
|
211
322
|
}
|
|
212
323
|
|
|
324
|
+
validateStatus(statusCode, statusMessage);
|
|
213
325
|
this.statusCode = statusCode;
|
|
214
326
|
if (statusMessage !== undefined) this.statusMessage = String(statusMessage);
|
|
215
327
|
|
|
@@ -259,18 +371,49 @@ class ServerResponse extends Writable {
|
|
|
259
371
|
|
|
260
372
|
write(chunk, encoding, callback) {
|
|
261
373
|
if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
|
|
374
|
+
this._wrote = true;
|
|
262
375
|
return super.write(chunk, encoding, callback);
|
|
263
376
|
}
|
|
264
377
|
|
|
265
378
|
end(data, encoding, callback) {
|
|
266
379
|
if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
|
|
267
380
|
if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
|
|
381
|
+
// The whole body, when nothing was written before it and the head has
|
|
382
|
+
// not gone: its length, for HTTP/1.1 to send as Content-Length rather
|
|
383
|
+
// than chunk it (node:http's end() does the same). HTTP/2 and HTTP/3
|
|
384
|
+
// frame the body themselves and do not read it.
|
|
385
|
+
if (!this.headersSent && !this._wrote && !this.writableEnded) {
|
|
386
|
+
var len = data == null ? 0
|
|
387
|
+
: typeof data === 'string' ? Buffer.byteLength(data, encoding)
|
|
388
|
+
: data.byteLength;
|
|
389
|
+
if (typeof len === 'number') this._endLength = len; // else write() rejects it
|
|
390
|
+
}
|
|
268
391
|
if (data != null) this.write(data, encoding);
|
|
269
392
|
return super.end(callback);
|
|
270
393
|
}
|
|
271
394
|
|
|
272
395
|
writeContinue() { /* HTTP/3 doesn't use 100-continue — stub */ }
|
|
273
396
|
|
|
397
|
+
/**
|
|
398
|
+
* 103 Early Hints (RFC 8297) — node:http's response.writeEarlyHints(hints[,
|
|
399
|
+
* callback]). `hints` is a header object; `link` a string or an array of
|
|
400
|
+
* them. The protocol layer that made this response replaces this with the
|
|
401
|
+
* real thing (h1_server, h2_server, h3_server); this one, on a response
|
|
402
|
+
* nothing wired, sends nothing and still calls back.
|
|
403
|
+
*/
|
|
404
|
+
writeEarlyHints(hints, callback) {
|
|
405
|
+
if (typeof callback === 'function') process.nextTick(callback);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* The response's priority, { urgency, incremental } (RFC 9218). HTTP/2 and
|
|
410
|
+
* HTTP/3 replace this with the real thing (h2_server, h3_server); over
|
|
411
|
+
* HTTP/1.1 a connection carries one response at a time, so there is
|
|
412
|
+
* nothing to order, and it does nothing. It was missing there: the same
|
|
413
|
+
* handler, reached over HTTP/1.1, threw a TypeError.
|
|
414
|
+
*/
|
|
415
|
+
setPriority(priority) {}
|
|
416
|
+
|
|
274
417
|
setTimeout(msecs, callback) {
|
|
275
418
|
if (callback) this.once('timeout', callback);
|
|
276
419
|
return this;
|
|
@@ -282,12 +425,22 @@ class ServerResponse extends Writable {
|
|
|
282
425
|
// ClientRequest — extends Writable
|
|
283
426
|
//
|
|
284
427
|
// Wraps outgoing HTTP request on the client side.
|
|
285
|
-
//
|
|
286
|
-
//
|
|
428
|
+
// The body flows as it is written, as node:http's does: held here only
|
|
429
|
+
// until a transport takes the request (_attachSink — the first of the
|
|
430
|
+
// protocols tried to be ready to send), then handed to it, with its
|
|
431
|
+
// backpressure (write() false, 'drain'). What was handed is kept, up to
|
|
432
|
+
// REPLAY_MAX, so that a request another protocol must carry after all — the
|
|
433
|
+
// first failed before the server processed it — can be sent again whole.
|
|
287
434
|
//
|
|
288
435
|
// The 'response' event delivers an IncomingMessage.
|
|
289
436
|
// ============================================================
|
|
290
437
|
|
|
438
|
+
// How much of a request body is kept, once handed to a transport, for
|
|
439
|
+
// sending again over another — what a request refused unprocessed (REFUSED_
|
|
440
|
+
// STREAM, H3_REQUEST_REJECTED, past a GOAWAY) or whose first protocol failed
|
|
441
|
+
// may need. Past it, the body streams without a copy.
|
|
442
|
+
var REPLAY_MAX = 64 * 1024;
|
|
443
|
+
|
|
291
444
|
class ClientRequest extends Writable {
|
|
292
445
|
|
|
293
446
|
constructor(options) {
|
|
@@ -309,6 +462,11 @@ class ClientRequest extends Writable {
|
|
|
309
462
|
|
|
310
463
|
options = options || {};
|
|
311
464
|
|
|
465
|
+
// node:http throws these from http.request() itself: the path (no CR, LF,
|
|
466
|
+
// space or other control character — CVE-2018-12116 was a request split
|
|
467
|
+
// through it) and the method (a token).
|
|
468
|
+
validateRequestTarget(options.path, options.method);
|
|
469
|
+
|
|
312
470
|
this.method = (options.method || 'GET').toUpperCase();
|
|
313
471
|
this.path = options.path || '/';
|
|
314
472
|
this.host = options.hostname || options.host || 'localhost';
|
|
@@ -327,8 +485,17 @@ class ClientRequest extends Writable {
|
|
|
327
485
|
for (var name in options.headers) this.setHeader(name, options.headers[name]);
|
|
328
486
|
}
|
|
329
487
|
|
|
330
|
-
//
|
|
488
|
+
// The body: held until a transport takes the request, then streamed to it.
|
|
331
489
|
this._bodyChunks = [];
|
|
490
|
+
this._bodyBuffered = 0;
|
|
491
|
+
this._pendingWriteCb = null; // a write held for room (before a transport)
|
|
492
|
+
this._sink = null; // the transport request carrying the body
|
|
493
|
+
this._sinkWaitCb = null; // a write waiting for the sink's 'drain'
|
|
494
|
+
this._replay = []; // what the sink was handed, while ≤ REPLAY_MAX
|
|
495
|
+
this._replayBytes = 0;
|
|
496
|
+
this._replayable = true;
|
|
497
|
+
this._trailers = null; // addTrailers()
|
|
498
|
+
this._priority = null; // setPriority()
|
|
332
499
|
|
|
333
500
|
// H3 internals — set by unified client after protocol selection
|
|
334
501
|
this._h3 = null;
|
|
@@ -346,11 +513,49 @@ class ClientRequest extends Writable {
|
|
|
346
513
|
|
|
347
514
|
this._timeoutTimer = null;
|
|
348
515
|
this._timeoutMs = 0;
|
|
516
|
+
|
|
517
|
+
// The response, once there is one (node:http's req.res), and how to stop
|
|
518
|
+
// each transport attempt that carries this request (see _destroy).
|
|
519
|
+
this.res = null;
|
|
520
|
+
this._cancels = [];
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ---- Cancellation ----
|
|
524
|
+
//
|
|
525
|
+
// destroy() / abort() stop the exchange where it is carried, as node:http's
|
|
526
|
+
// do by destroying the socket. Without this they only marked the request:
|
|
527
|
+
// the transport went on, and the response kept downloading in the
|
|
528
|
+
// background. Each transport attempt registers how to stop itself — over
|
|
529
|
+
// HTTP/3 and HTTP/2 that is the request's stream, not the (shared)
|
|
530
|
+
// connection; over HTTP/1.1, the socket, which cannot be reused mid-exchange.
|
|
531
|
+
|
|
532
|
+
_onCancel(fn) {
|
|
533
|
+
if (this.destroyed) { fn(); return; }
|
|
534
|
+
this._cancels.push(fn);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// node:http: destroyed before a response, the request fails with 'socket
|
|
538
|
+
// hang up' (ECONNRESET) unless given its own error.
|
|
539
|
+
destroy(err) {
|
|
540
|
+
if (this.destroyed) return this;
|
|
541
|
+
if (!err && !this.res && !this.aborted) { err = new Error('socket hang up'); err.code = 'ECONNRESET'; }
|
|
542
|
+
return super.destroy(err);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
_destroy(err, cb) {
|
|
546
|
+
var cancels = this._cancels;
|
|
547
|
+
this._cancels = [];
|
|
548
|
+
for (var i = 0; i < cancels.length; i++) { try { cancels[i](); } catch (e) {} }
|
|
549
|
+
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
|
|
550
|
+
if (this.res && !this.res.complete) this.res._abort(err && err.code !== 'ECONNRESET' ? err : null);
|
|
551
|
+
cb(err);
|
|
349
552
|
}
|
|
350
553
|
|
|
351
554
|
// ---- Header management ----
|
|
352
555
|
|
|
353
556
|
setHeader(name, value) {
|
|
557
|
+
validateHeaderName(name);
|
|
558
|
+
validateHeaderValue(name, value);
|
|
354
559
|
var lower = name.toLowerCase();
|
|
355
560
|
this._headers[lower] = value;
|
|
356
561
|
this._headerNames[lower] = name;
|
|
@@ -388,16 +593,93 @@ class ClientRequest extends Writable {
|
|
|
388
593
|
if (Buffer.isBuffer(chunk)) {
|
|
389
594
|
chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
390
595
|
}
|
|
596
|
+
if (this._sink) { this._toSink(chunk, callback); return; }
|
|
597
|
+
// No transport yet (still connecting): held — and the writer told to wait
|
|
598
|
+
// past the high-water mark, as a socket not yet connected would.
|
|
391
599
|
this._bodyChunks.push(chunk);
|
|
392
|
-
|
|
600
|
+
this._bodyBuffered += chunk.byteLength;
|
|
601
|
+
if (this._bodyBuffered < this.writableHighWaterMark) callback();
|
|
602
|
+
else this._pendingWriteCb = callback;
|
|
393
603
|
}
|
|
394
604
|
|
|
395
605
|
_final(callback) {
|
|
396
606
|
this.finished = true;
|
|
397
|
-
this.
|
|
607
|
+
if (this._sink) this._sink.end(null, this._trailers);
|
|
398
608
|
callback();
|
|
399
609
|
}
|
|
400
610
|
|
|
611
|
+
// node:http's addTrailers(): fields sent after the body (a HEADERS frame
|
|
612
|
+
// over HTTP/2 and HTTP/3; with chunked encoding over HTTP/1.1). An object,
|
|
613
|
+
// or a list of [name, value] pairs.
|
|
614
|
+
addTrailers(headers) {
|
|
615
|
+
var t = this._trailers || (this._trailers = {});
|
|
616
|
+
var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
|
|
617
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
618
|
+
var name = pairs[i][0], value = pairs[i][1];
|
|
619
|
+
validateHeaderName(name);
|
|
620
|
+
validateHeaderValue(name, value);
|
|
621
|
+
t[String(name).toLowerCase()] = value;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// The request's priority (RFC 9218): { urgency: 0-7 (3), incremental
|
|
626
|
+
// (false) } — the `priority` header while the request has not gone, a
|
|
627
|
+
// PRIORITY_UPDATE after it (HTTP/2, HTTP/3), as res.setPriority is the
|
|
628
|
+
// server's. Normalized the same way.
|
|
629
|
+
setPriority(p) {
|
|
630
|
+
this._priority = responsePriority(p);
|
|
631
|
+
if (this._sink && typeof this._sink.setPriority === 'function') this._sink.setPriority(this._priority);
|
|
632
|
+
return this;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ---- The transport that carries the body (client.js) ----
|
|
636
|
+
|
|
637
|
+
// `sink` (a transport's request: write → more?, 'drain', end(chunk,
|
|
638
|
+
// trailers)) takes the request: what is held goes to it, and from now on
|
|
639
|
+
// every write.
|
|
640
|
+
_attachSink(sink) {
|
|
641
|
+
var self = this;
|
|
642
|
+
this._sink = sink;
|
|
643
|
+
if (this._priority && typeof sink.setPriority === 'function') sink.setPriority(this._priority);
|
|
644
|
+
sink.on('drain', function () {
|
|
645
|
+
if (self._sink !== sink || !self._sinkWaitCb) return;
|
|
646
|
+
var cb = self._sinkWaitCb; self._sinkWaitCb = null; cb();
|
|
647
|
+
});
|
|
648
|
+
var held = this._bodyChunks, more = true;
|
|
649
|
+
this._bodyChunks = []; this._bodyBuffered = 0;
|
|
650
|
+
for (var i = 0; i < held.length; i++) { this._remember(held[i]); more = sink.write(held[i]); }
|
|
651
|
+
if (this._pendingWriteCb) {
|
|
652
|
+
var cb = this._pendingWriteCb; this._pendingWriteCb = null;
|
|
653
|
+
if (more) cb(); else this._sinkWaitCb = cb;
|
|
654
|
+
}
|
|
655
|
+
if (this.finished) sink.end(null, this._trailers);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// The transport failed before the server processed the request: the body
|
|
659
|
+
// back here, whole, for the next — or false if it cannot be (more than
|
|
660
|
+
// REPLAY_MAX was handed over: not kept).
|
|
661
|
+
_detachSink() {
|
|
662
|
+
if (!this._replayable) return false;
|
|
663
|
+
this._sink = null;
|
|
664
|
+
this._bodyChunks = this._replay; this._bodyBuffered = this._replayBytes;
|
|
665
|
+
this._replay = []; this._replayBytes = 0;
|
|
666
|
+
if (this._sinkWaitCb) { this._pendingWriteCb = this._sinkWaitCb; this._sinkWaitCb = null; }
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
_toSink(chunk, callback) {
|
|
671
|
+
this._remember(chunk);
|
|
672
|
+
if (this._sink.write(chunk)) callback();
|
|
673
|
+
else this._sinkWaitCb = callback;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
_remember(chunk) {
|
|
677
|
+
if (!this._replayable) return;
|
|
678
|
+
this._replayBytes += chunk.byteLength;
|
|
679
|
+
if (this._replayBytes > REPLAY_MAX) { this._replayable = false; this._replay = []; this._replayBytes = 0; return; }
|
|
680
|
+
this._replay.push(chunk);
|
|
681
|
+
}
|
|
682
|
+
|
|
401
683
|
// ---- Convenience overrides ----
|
|
402
684
|
|
|
403
685
|
write(chunk, encoding, callback) {
|
|
@@ -412,9 +694,13 @@ class ClientRequest extends Writable {
|
|
|
412
694
|
return super.end(callback);
|
|
413
695
|
}
|
|
414
696
|
|
|
697
|
+
// node:http: 'abort', then 'close' — no error.
|
|
415
698
|
abort() {
|
|
699
|
+
if (this.aborted || this.destroyed) return;
|
|
416
700
|
this.aborted = true;
|
|
417
|
-
|
|
701
|
+
var self = this;
|
|
702
|
+
process.nextTick(function () { self.emit('abort'); });
|
|
703
|
+
this.destroy();
|
|
418
704
|
}
|
|
419
705
|
|
|
420
706
|
setTimeout(msecs, callback) {
|
|
@@ -426,24 +712,6 @@ class ClientRequest extends Writable {
|
|
|
426
712
|
}
|
|
427
713
|
return this;
|
|
428
714
|
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Collect all buffered body chunks into one Uint8Array.
|
|
432
|
-
* Returns null if nothing was written (GET / HEAD).
|
|
433
|
-
*/
|
|
434
|
-
_getBody() {
|
|
435
|
-
if (this._bodyChunks.length === 0) return null;
|
|
436
|
-
if (this._bodyChunks.length === 1) return this._bodyChunks[0];
|
|
437
|
-
var total = 0;
|
|
438
|
-
for (var i = 0; i < this._bodyChunks.length; i++) total += this._bodyChunks[i].byteLength;
|
|
439
|
-
var merged = new Uint8Array(total);
|
|
440
|
-
var off = 0;
|
|
441
|
-
for (var i = 0; i < this._bodyChunks.length; i++) {
|
|
442
|
-
merged.set(this._bodyChunks[i], off);
|
|
443
|
-
off += this._bodyChunks[i].byteLength;
|
|
444
|
-
}
|
|
445
|
-
return merged;
|
|
446
|
-
}
|
|
447
715
|
}
|
|
448
716
|
|
|
449
717
|
|
|
@@ -452,6 +720,64 @@ class ClientRequest extends Writable {
|
|
|
452
720
|
// Must be a real Stream so Node's eos() / Express don't crash.
|
|
453
721
|
// ============================================================
|
|
454
722
|
|
|
723
|
+
/**
|
|
724
|
+
* Make an Express application (or anything built the same way) run on these
|
|
725
|
+
* streams.
|
|
726
|
+
*
|
|
727
|
+
* Express does not use the req and res it is handed as they are: on every
|
|
728
|
+
* request it swaps their prototype for its own (Object.setPrototypeOf(req,
|
|
729
|
+
* app.request)), and that chain ends in node:http's IncomingMessage and
|
|
730
|
+
* ServerResponse. Every method of these classes is gone after that — a
|
|
731
|
+
* request body arrived through _pushData, which no longer existed, and the
|
|
732
|
+
* server fell over on the first POST. The response survived only because its
|
|
733
|
+
* methods are also bound onto each instance (see the constructor), and even
|
|
734
|
+
* so it ran on node:http's getters for its state.
|
|
735
|
+
*
|
|
736
|
+
* So the chain is re-parented instead, once per application:
|
|
737
|
+
*
|
|
738
|
+
* before app.response → express's response methods → http.ServerResponse
|
|
739
|
+
* after app.response → a copy of those methods → quico ServerResponse
|
|
740
|
+
*
|
|
741
|
+
* Express's methods — req.get, res.json, res.send, res.cookie,
|
|
742
|
+
* express.static — sit on top and call the ones below, which are now ours:
|
|
743
|
+
* the same object, on HTTP/1.1, HTTP/2 and HTTP/3.
|
|
744
|
+
*
|
|
745
|
+
* A COPY, because the object holding Express's methods is shared by every
|
|
746
|
+
* Express application in the process: re-parenting it would move them all,
|
|
747
|
+
* including any served by node:http. The copy belongs to this application
|
|
748
|
+
* alone. A sub-application inherits from its parent's objects when it is
|
|
749
|
+
* mounted, so the root is the only one that needs it.
|
|
750
|
+
*
|
|
751
|
+
* Recognised by shape — a function with `request` and `response` objects —
|
|
752
|
+
* not by name; left alone when already adopted or not that shape. Returns
|
|
753
|
+
* the handler, for chaining.
|
|
754
|
+
*/
|
|
755
|
+
function adoptApp(handler) {
|
|
756
|
+
if (typeof handler !== 'function') return handler;
|
|
757
|
+
reparent(handler.request, IncomingMessage);
|
|
758
|
+
reparent(handler.response, ServerResponse);
|
|
759
|
+
return handler;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Put `Base` under obj's prototype chain in place of the class it ends in: the
|
|
763
|
+
// objects between obj and the first class prototype (one owning a
|
|
764
|
+
// `constructor`) are copied, the last copy inheriting from Base.prototype.
|
|
765
|
+
function reparent(obj, Base) {
|
|
766
|
+
if (!obj || typeof obj !== 'object' || obj instanceof Base) return;
|
|
767
|
+
var between = [];
|
|
768
|
+
var p = Object.getPrototypeOf(obj);
|
|
769
|
+
while (p && !Object.prototype.hasOwnProperty.call(p, 'constructor')) {
|
|
770
|
+
between.push(p);
|
|
771
|
+
p = Object.getPrototypeOf(p);
|
|
772
|
+
}
|
|
773
|
+
if (!p || p === Object.prototype) return; // not a class chain: not ours to change
|
|
774
|
+
var top = Base.prototype;
|
|
775
|
+
for (var i = between.length - 1; i >= 0; i--) {
|
|
776
|
+
top = Object.create(top, Object.getOwnPropertyDescriptors(between[i]));
|
|
777
|
+
}
|
|
778
|
+
Object.setPrototypeOf(obj, top);
|
|
779
|
+
}
|
|
780
|
+
|
|
455
781
|
function _makeSocketStub() {
|
|
456
782
|
var sock = new Duplex({
|
|
457
783
|
read(_size) { /* no-op — no data from QUIC socket */ },
|
|
@@ -482,4 +808,4 @@ function _makeSocketStub() {
|
|
|
482
808
|
}
|
|
483
809
|
|
|
484
810
|
|
|
485
|
-
export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub };
|
|
811
|
+
export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub, adoptApp };
|