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/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,34 +53,59 @@ 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();
|
|
58
61
|
this.connection = this.socket;
|
|
62
|
+
this.client = this.socket; // node:http's older alias
|
|
63
|
+
// node:http's: true on a request handed to 'upgrade' (an HTTP/1.1
|
|
64
|
+
// Upgrade, an extended CONNECT over HTTP/2 and HTTP/3).
|
|
65
|
+
this.upgrade = false;
|
|
59
66
|
|
|
60
67
|
// H3 stream id (internal)
|
|
61
68
|
this._streamId = options.streamId != null ? options.streamId : null;
|
|
62
69
|
this._timeoutTimer = null;
|
|
63
70
|
|
|
64
|
-
// Build rawHeaders
|
|
65
|
-
|
|
66
|
-
|
|
71
|
+
// Build rawHeaders: one name and value per field line, as node:http has
|
|
72
|
+
// them — the field lines as they came when the decoder kept them
|
|
73
|
+
// (validateFieldSection's _rawHeaders); otherwise from the map, a list
|
|
74
|
+
// (Set-Cookie) one entry per value. String(list) made two cookies one
|
|
75
|
+
// "a=1,b=2".
|
|
76
|
+
var rawList = this.headers._rawHeaders;
|
|
77
|
+
if (Array.isArray(rawList)) {
|
|
78
|
+
for (var ri = 0; ri < rawList.length; ri++) this.rawHeaders.push(rawList[ri]);
|
|
79
|
+
} else {
|
|
80
|
+
for (var name in this.headers) {
|
|
81
|
+
var hv = this.headers[name];
|
|
82
|
+
if (Array.isArray(hv)) { for (var hi = 0; hi < hv.length; hi++) this.rawHeaders.push(name, String(hv[hi])); }
|
|
83
|
+
else this.rawHeaders.push(name, String(hv));
|
|
84
|
+
}
|
|
67
85
|
}
|
|
68
86
|
}
|
|
69
87
|
|
|
88
|
+
// node:http's headersDistinct / trailersDistinct: each name (lower case)
|
|
89
|
+
// with every value it came with, as an array, nothing joined — from the
|
|
90
|
+
// field lines as they arrived (rawHeaders, rawTrailers).
|
|
91
|
+
get headersDistinct() { return distinctFields(this.rawHeaders); }
|
|
92
|
+
get trailersDistinct() { return distinctFields(this.rawTrailers); }
|
|
93
|
+
|
|
70
94
|
// Readable requires _read — data is pushed externally, so no-op.
|
|
71
95
|
_read(_size) { /* no-op */ }
|
|
72
96
|
|
|
73
97
|
/**
|
|
74
98
|
* Called by H3 layer when a body chunk arrives.
|
|
75
99
|
*/
|
|
100
|
+
// push()'s answer: false once the buffer is full — the caller stops
|
|
101
|
+
// reading until _read asks for more.
|
|
76
102
|
_pushData(chunk) {
|
|
77
|
-
if (this.complete) return;
|
|
103
|
+
if (this.complete) return true;
|
|
78
104
|
if (typeof chunk === 'string') chunk = Buffer.from(chunk);
|
|
79
105
|
if (chunk instanceof Uint8Array && !Buffer.isBuffer(chunk)) {
|
|
80
106
|
chunk = Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
81
107
|
}
|
|
82
|
-
this.push(chunk);
|
|
108
|
+
return this.push(chunk);
|
|
83
109
|
}
|
|
84
110
|
|
|
85
111
|
/**
|
|
@@ -92,14 +118,31 @@ class IncomingMessage extends Readable {
|
|
|
92
118
|
}
|
|
93
119
|
|
|
94
120
|
/**
|
|
95
|
-
*
|
|
121
|
+
* The exchange ended before its body did — the peer reset the stream or
|
|
122
|
+
* went away, or (client side) req.destroy(). The one place this is decided,
|
|
123
|
+
* as node:http's IncomingMessage does it:
|
|
124
|
+
*
|
|
125
|
+
* 'aborted' — always, once;
|
|
126
|
+
* 'error' — ('aborted', ECONNRESET, or the cause given) only to a
|
|
127
|
+
* listener that asked for one: unheard, it would take the
|
|
128
|
+
* process down, and node:http teaches 'aborted'/'close';
|
|
129
|
+
* 'close' — and never 'end', which would say the body is whole.
|
|
130
|
+
*
|
|
131
|
+
* It ended the stream with push(null) — 'end', a cut-off upload read as a
|
|
132
|
+
* complete one — and each caller decided the 'error' question for itself,
|
|
133
|
+
* one (h1_server) not at all, so a client vanishing mid-upload threw.
|
|
134
|
+
* A body that had already arrived in full is left as it is: 'aborted', and
|
|
135
|
+
* nothing to cut.
|
|
96
136
|
*/
|
|
97
137
|
_abort(err) {
|
|
98
|
-
if (this.
|
|
138
|
+
if (this.aborted) return;
|
|
99
139
|
this.aborted = true;
|
|
140
|
+
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
|
|
141
|
+
this.emit('aborted');
|
|
142
|
+
if (this.complete) return;
|
|
100
143
|
this.complete = true;
|
|
101
|
-
if (err)
|
|
102
|
-
|
|
144
|
+
if (!err) { err = new Error('aborted'); err.code = 'ECONNRESET'; }
|
|
145
|
+
this.destroy(this.listenerCount('error') > 0 ? err : null);
|
|
103
146
|
}
|
|
104
147
|
|
|
105
148
|
setTimeout(msecs, callback) {
|
|
@@ -110,6 +153,32 @@ class IncomingMessage extends Readable {
|
|
|
110
153
|
}
|
|
111
154
|
return this;
|
|
112
155
|
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* What destroy() by the application means on the wire, given by the
|
|
159
|
+
* transport that made this message: a request's body the server no longer
|
|
160
|
+
* wants (STOP_SENDING / RST_STREAM; over HTTP/1.1, the socket), a response
|
|
161
|
+
* the client gave up on mid-way. Not run when the stream itself ended it
|
|
162
|
+
* (_abort: the peer's reset, the connection gone); each hook decides what
|
|
163
|
+
* is left to stop (a body that arrived whole: nothing). destroy() was
|
|
164
|
+
* local only: the body kept coming, at full speed once its credit was
|
|
165
|
+
* released, into a message nobody read.
|
|
166
|
+
*/
|
|
167
|
+
_onDestroy(fn) {
|
|
168
|
+
(this._destroyHooks || (this._destroyHooks = [])).push(fn);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
_destroy(err, cb) {
|
|
172
|
+
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
|
|
173
|
+
var hooks = this._destroyHooks;
|
|
174
|
+
this._destroyHooks = null;
|
|
175
|
+
// Not after 'end': that is autoDestroy closing a body read to the end,
|
|
176
|
+
// not the application giving anything up.
|
|
177
|
+
if (hooks && !this.aborted && !this.readableEnded) {
|
|
178
|
+
for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
|
|
179
|
+
}
|
|
180
|
+
cb(err);
|
|
181
|
+
}
|
|
113
182
|
}
|
|
114
183
|
|
|
115
184
|
|
|
@@ -128,11 +197,19 @@ class ServerResponse extends Writable {
|
|
|
128
197
|
options = options || {};
|
|
129
198
|
|
|
130
199
|
this.statusCode = 200;
|
|
131
|
-
|
|
200
|
+
// Unset, as node:http has it: the standard reason phrase for whatever the
|
|
201
|
+
// status ends up being. It was 'OK', so a status set without one —
|
|
202
|
+
// res.statusCode = 404, writeHead(425, headers) — went out as '404 OK'.
|
|
203
|
+
this.statusMessage = undefined;
|
|
132
204
|
this.headersSent = false;
|
|
133
205
|
this.finished = false;
|
|
134
206
|
this._headers = {};
|
|
135
207
|
this._headerNames = {}; // lowercase → original-case
|
|
208
|
+
// node:http's: a Date header unless false (RFC 9110 §6.6.1); and, when
|
|
209
|
+
// true, a body that does not match its Content-Length throws.
|
|
210
|
+
this.sendDate = true;
|
|
211
|
+
this.strictContentLength = false;
|
|
212
|
+
this._bodyBytes = 0;
|
|
136
213
|
|
|
137
214
|
// H3 references (set by server when creating the response)
|
|
138
215
|
this._streamId = options.streamId != null ? options.streamId : null;
|
|
@@ -144,9 +221,11 @@ class ServerResponse extends Writable {
|
|
|
144
221
|
this.socket = options.socket || _makeSocketStub();
|
|
145
222
|
this.connection = this.socket;
|
|
146
223
|
|
|
224
|
+
// 'close' comes from the Writable itself (autoDestroy), once, after
|
|
225
|
+
// 'finish' — node's order. It was also emitted here, from a 'finish'
|
|
226
|
+
// listener registered first: the application saw close, finish, close.
|
|
147
227
|
this.on('finish', () => {
|
|
148
228
|
this.finished = true;
|
|
149
|
-
this.emit('close');
|
|
150
229
|
});
|
|
151
230
|
|
|
152
231
|
// Express does res.__proto__ = app.response (http.ServerResponse.prototype)
|
|
@@ -157,6 +236,9 @@ class ServerResponse extends Writable {
|
|
|
157
236
|
this.getHeaderNames = this.getHeaderNames.bind(this);
|
|
158
237
|
this.hasHeader = this.hasHeader.bind(this);
|
|
159
238
|
this.removeHeader = this.removeHeader.bind(this);
|
|
239
|
+
this.appendHeader = this.appendHeader.bind(this);
|
|
240
|
+
this.setHeaders = this.setHeaders.bind(this);
|
|
241
|
+
this.getRawHeaderNames = this.getRawHeaderNames.bind(this);
|
|
160
242
|
this.writeHead = this.writeHead.bind(this);
|
|
161
243
|
this.flushHeaders = this.flushHeaders.bind(this);
|
|
162
244
|
this._flushHeaders = this._flushHeaders.bind(this);
|
|
@@ -164,10 +246,49 @@ class ServerResponse extends Writable {
|
|
|
164
246
|
this.end = this.end.bind(this);
|
|
165
247
|
}
|
|
166
248
|
|
|
249
|
+
/**
|
|
250
|
+
* The peer is gone — the stream reset, the connection lost — while this
|
|
251
|
+
* response was still being written. As node:http: 'close', once; later
|
|
252
|
+
* writes are dropped without an error, so a handler mid-flight is not
|
|
253
|
+
* taken down, and a pipe() into the response stops. Without it a response
|
|
254
|
+
* whose connection died never closed, and a pipe() kept reading its source
|
|
255
|
+
* into a connection that was gone.
|
|
256
|
+
*/
|
|
257
|
+
_abortResponse() {
|
|
258
|
+
if (this.writableFinished || this.destroyed) return;
|
|
259
|
+
this._aborted = true;
|
|
260
|
+
this.destroy();
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* res.destroy([err]) by the application, mid-response: the transport ends
|
|
265
|
+
* the stream so the client hears it — RESET_STREAM over HTTP/3, RST_STREAM
|
|
266
|
+
* over HTTP/2, the socket over HTTP/1.1 (as node:http) — with an internal
|
|
267
|
+
* error's code when there is an err (a failed pipeline), a cancel's
|
|
268
|
+
* otherwise. It was local only: the client waited for the rest of a
|
|
269
|
+
* response that would never come, and over HTTP/3 and HTTP/2 the stream
|
|
270
|
+
* stayed open until the connection closed. As node:http, the err is not
|
|
271
|
+
* re-emitted as 'error' to a response nobody listens to.
|
|
272
|
+
*/
|
|
273
|
+
_onDestroy(fn) {
|
|
274
|
+
(this._destroyHooks || (this._destroyHooks = [])).push(fn);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
_destroy(err, cb) {
|
|
278
|
+
var hooks = this._destroyHooks;
|
|
279
|
+
this._destroyHooks = null;
|
|
280
|
+
if (hooks && !this._aborted && !this.writableFinished) {
|
|
281
|
+
for (var i = 0; i < hooks.length; i++) { try { hooks[i](err); } catch (e) {} }
|
|
282
|
+
}
|
|
283
|
+
cb(err && this.listenerCount('error') > 0 ? err : null);
|
|
284
|
+
}
|
|
285
|
+
|
|
167
286
|
// ---- Header management (mirrors node:http) ----
|
|
168
287
|
|
|
169
288
|
setHeader(name, value) {
|
|
170
|
-
if (this.headersSent) throw
|
|
289
|
+
if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
|
|
290
|
+
validateHeaderName(name);
|
|
291
|
+
validateHeaderValue(name, value);
|
|
171
292
|
var lower = name.toLowerCase();
|
|
172
293
|
this._headers[lower] = value;
|
|
173
294
|
this._headerNames[lower] = name;
|
|
@@ -178,8 +299,10 @@ class ServerResponse extends Writable {
|
|
|
178
299
|
return this._headers[name.toLowerCase()];
|
|
179
300
|
}
|
|
180
301
|
|
|
302
|
+
// An object with no prototype, as node:http's: a header named
|
|
303
|
+
// "constructor" or "__proto__" is a header, not a property it inherits.
|
|
181
304
|
getHeaders() {
|
|
182
|
-
var out =
|
|
305
|
+
var out = Object.create(null);
|
|
183
306
|
for (var k in this._headers) out[k] = this._headers[k];
|
|
184
307
|
return out;
|
|
185
308
|
}
|
|
@@ -188,18 +311,79 @@ class ServerResponse extends Writable {
|
|
|
188
311
|
return Object.keys(this._headers);
|
|
189
312
|
}
|
|
190
313
|
|
|
314
|
+
// node:http's: the names as they were set, in their case.
|
|
315
|
+
getRawHeaderNames() {
|
|
316
|
+
var out = [];
|
|
317
|
+
for (var k in this._headers) out.push(this._headerNames[k] || k);
|
|
318
|
+
return out;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// node:http's: a value added to a header's (both kept), not in its place.
|
|
322
|
+
appendHeader(name, value) {
|
|
323
|
+
if (this.headersSent) throw headersSentError('append', this.req && this.req.httpVersion);
|
|
324
|
+
return this._appendHeader(name, value);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// node:http's: several at once, from a Headers or a Map — each replacing
|
|
328
|
+
// that name's value (Set-Cookie: every one of a Headers' cookies).
|
|
329
|
+
setHeaders(headers) {
|
|
330
|
+
if (this.headersSent) throw headersSentError('set', this.req && this.req.httpVersion);
|
|
331
|
+
var isHeaders = typeof Headers !== 'undefined' && headers instanceof Headers;
|
|
332
|
+
if (!isHeaders && !(headers instanceof Map)) {
|
|
333
|
+
var e = new TypeError('The "headers" argument must be an instance of Headers or Map');
|
|
334
|
+
e.code = 'ERR_INVALID_ARG_TYPE';
|
|
335
|
+
throw e;
|
|
336
|
+
}
|
|
337
|
+
var self = this, cookies = null;
|
|
338
|
+
headers.forEach(function (value, name) {
|
|
339
|
+
if (isHeaders && String(name).toLowerCase() === 'set-cookie') { cookies = headers.getSetCookie(); return; }
|
|
340
|
+
self.setHeader(name, value);
|
|
341
|
+
});
|
|
342
|
+
if (cookies) this.setHeader('set-cookie', cookies);
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
|
|
191
346
|
hasHeader(name) {
|
|
192
347
|
return name.toLowerCase() in this._headers;
|
|
193
348
|
}
|
|
194
349
|
|
|
195
350
|
removeHeader(name) {
|
|
196
|
-
if (this.headersSent) throw
|
|
351
|
+
if (this.headersSent) throw headersSentError('remove', this.req && this.req.httpVersion);
|
|
197
352
|
var lower = name.toLowerCase();
|
|
198
353
|
delete this._headers[lower];
|
|
199
354
|
delete this._headerNames[lower];
|
|
200
355
|
return this;
|
|
201
356
|
}
|
|
202
357
|
|
|
358
|
+
// A value added to what a name has (kept as a list), not in its place.
|
|
359
|
+
_appendHeader(name, value) {
|
|
360
|
+
var lower = String(name).toLowerCase();
|
|
361
|
+
if (!(lower in this._headers)) return this.setHeader(name, value);
|
|
362
|
+
validateHeaderName(name);
|
|
363
|
+
validateHeaderValue(name, value);
|
|
364
|
+
this._headers[lower] = [].concat(this._headers[lower], value);
|
|
365
|
+
return this;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* node:http's res.addTrailers(headers): fields sent after the body — an
|
|
370
|
+
* object, or an array of [name, value] pairs. Over HTTP/1.1 they go in the
|
|
371
|
+
* last chunk (chunked responses only, as node); over HTTP/2 as a trailing
|
|
372
|
+
* HEADERS frame. The layers read res.trailers, but the method an application
|
|
373
|
+
* calls was missing: it threw a TypeError inside the handler, and over
|
|
374
|
+
* HTTP/2 that tore down the whole connection.
|
|
375
|
+
*/
|
|
376
|
+
addTrailers(headers) {
|
|
377
|
+
if (!this.trailers) this.trailers = {};
|
|
378
|
+
var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
|
|
379
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
380
|
+
var name = pairs[i][0], value = pairs[i][1];
|
|
381
|
+
validateHeaderName(name, 'Trailer name');
|
|
382
|
+
validateHeaderValue(name, value);
|
|
383
|
+
this.trailers[String(name).toLowerCase()] = value;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
203
387
|
// ---- writeHead ----
|
|
204
388
|
|
|
205
389
|
writeHead(statusCode, statusMessage, headers) {
|
|
@@ -210,10 +394,26 @@ class ServerResponse extends Writable {
|
|
|
210
394
|
statusMessage = undefined;
|
|
211
395
|
}
|
|
212
396
|
|
|
397
|
+
validateStatus(statusCode, statusMessage);
|
|
213
398
|
this.statusCode = statusCode;
|
|
214
399
|
if (statusMessage !== undefined) this.statusMessage = String(statusMessage);
|
|
215
400
|
|
|
216
|
-
|
|
401
|
+
// node:http's three forms: an object; a flat array [name, value, name,
|
|
402
|
+
// value]; an array of [name, value] pairs. The arrays may repeat a name
|
|
403
|
+
// and every value is kept (two Set-Cookie lines). An array went through
|
|
404
|
+
// for…in: headers named "0", "1", … — over HTTP/3 the response failed.
|
|
405
|
+
if (Array.isArray(headers)) {
|
|
406
|
+
var tuples = headers.length > 0 && Array.isArray(headers[0]);
|
|
407
|
+
if (!tuples && headers.length % 2 !== 0) {
|
|
408
|
+
var bad = new TypeError('The argument \'headers\' is invalid: an array of names and values must have an even length');
|
|
409
|
+
bad.code = 'ERR_INVALID_ARG_VALUE';
|
|
410
|
+
throw bad;
|
|
411
|
+
}
|
|
412
|
+
for (var i = 0; i < headers.length; i += tuples ? 1 : 2) {
|
|
413
|
+
var hn = tuples ? headers[i][0] : headers[i], hv = tuples ? headers[i][1] : headers[i + 1];
|
|
414
|
+
if (hn) this._appendHeader(hn, hv);
|
|
415
|
+
}
|
|
416
|
+
} else if (typeof headers === 'object' && headers !== null) {
|
|
217
417
|
for (var name in headers) this.setHeader(name, headers[name]);
|
|
218
418
|
}
|
|
219
419
|
|
|
@@ -259,18 +459,95 @@ class ServerResponse extends Writable {
|
|
|
259
459
|
|
|
260
460
|
write(chunk, encoding, callback) {
|
|
261
461
|
if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
|
|
462
|
+
this._wrote = true;
|
|
463
|
+
this._countBody(chunk, encoding, false);
|
|
262
464
|
return super.write(chunk, encoding, callback);
|
|
263
465
|
}
|
|
264
466
|
|
|
467
|
+
// strictContentLength (node:http's): the body may not go past its
|
|
468
|
+
// Content-Length, nor end short of it — ERR_HTTP_CONTENT_LENGTH_MISMATCH,
|
|
469
|
+
// thrown from the write() or end() that shows it.
|
|
470
|
+
// Counted only once checked: a write that threw sent nothing.
|
|
471
|
+
_countBody(chunk, encoding, ending) {
|
|
472
|
+
var total = this._bodyBytes;
|
|
473
|
+
if (chunk != null && !this.writableEnded) {
|
|
474
|
+
total += typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : (chunk.byteLength || 0);
|
|
475
|
+
}
|
|
476
|
+
var declared = this.strictContentLength ? this.getHeader('content-length') : undefined;
|
|
477
|
+
if (declared !== undefined && !Array.isArray(declared) && !(this.req && this.req.method === 'HEAD') && Number(declared) >= 0) {
|
|
478
|
+
declared = Number(declared);
|
|
479
|
+
if (total > declared || (ending && total !== declared)) {
|
|
480
|
+
var e = new Error('Response body\'s content-length of ' + total + ' byte(s) does not match the content-length of ' + declared + ' byte(s) set in header');
|
|
481
|
+
e.code = 'ERR_HTTP_CONTENT_LENGTH_MISMATCH';
|
|
482
|
+
throw e;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
this._bodyBytes = total;
|
|
486
|
+
}
|
|
487
|
+
|
|
265
488
|
end(data, encoding, callback) {
|
|
266
489
|
if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
|
|
267
490
|
if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
|
|
268
|
-
if (
|
|
269
|
-
|
|
491
|
+
if (!this.writableEnded) {
|
|
492
|
+
if (data != null) this._countBody(data, encoding, true);
|
|
493
|
+
else this._countBody(null, null, true);
|
|
494
|
+
}
|
|
495
|
+
// The whole body, when nothing was written before it and the head has
|
|
496
|
+
// not gone: its length, for HTTP/1.1 to send as Content-Length rather
|
|
497
|
+
// than chunk it (node:http's end() does the same). HTTP/2 and HTTP/3
|
|
498
|
+
// frame the body themselves and do not read it.
|
|
499
|
+
if (!this.headersSent && !this._wrote && !this.writableEnded) {
|
|
500
|
+
var len = data == null ? 0
|
|
501
|
+
: typeof data === 'string' ? Buffer.byteLength(data, encoding)
|
|
502
|
+
: data.byteLength;
|
|
503
|
+
if (typeof len === 'number') this._endLength = len; // else write() rejects it
|
|
504
|
+
}
|
|
505
|
+
if (data != null) this._writeCounted(data, encoding);
|
|
506
|
+
// The callback runs at 'finish', after the listeners already on it — as
|
|
507
|
+
// node:http's end(cb), a once('finish'). Given to Writable's end() it
|
|
508
|
+
// ran before them.
|
|
509
|
+
if (typeof callback === 'function') {
|
|
510
|
+
if (this.writableFinished) process.nextTick(callback);
|
|
511
|
+
else this.once('finish', callback);
|
|
512
|
+
}
|
|
513
|
+
super.end();
|
|
514
|
+
return this;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// The last write, its bytes already counted by end().
|
|
518
|
+
_writeCounted(data, encoding) {
|
|
519
|
+
this._wrote = true;
|
|
520
|
+
return super.write(data, encoding);
|
|
270
521
|
}
|
|
271
522
|
|
|
272
523
|
writeContinue() { /* HTTP/3 doesn't use 100-continue — stub */ }
|
|
273
524
|
|
|
525
|
+
/**
|
|
526
|
+
* 103 Early Hints (RFC 8297) — node:http's response.writeEarlyHints(hints[,
|
|
527
|
+
* callback]). `hints` is a header object; `link` a string or an array of
|
|
528
|
+
* them. The protocol layer that made this response replaces this with the
|
|
529
|
+
* real thing (h1_server, h2_server, h3_server); this one, on a response
|
|
530
|
+
* nothing wired, sends nothing and still calls back.
|
|
531
|
+
*/
|
|
532
|
+
writeEarlyHints(hints, callback) {
|
|
533
|
+
if (typeof callback === 'function') process.nextTick(callback);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* 102 Processing — node:http's response.writeProcessing(). Replaced by the
|
|
538
|
+
* layer that made the response, as writeEarlyHints is; here, nothing.
|
|
539
|
+
*/
|
|
540
|
+
writeProcessing() {}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* The response's priority, { urgency, incremental } (RFC 9218). HTTP/2 and
|
|
544
|
+
* HTTP/3 replace this with the real thing (h2_server, h3_server); over
|
|
545
|
+
* HTTP/1.1 a connection carries one response at a time, so there is
|
|
546
|
+
* nothing to order, and it does nothing. It was missing there: the same
|
|
547
|
+
* handler, reached over HTTP/1.1, threw a TypeError.
|
|
548
|
+
*/
|
|
549
|
+
setPriority(priority) {}
|
|
550
|
+
|
|
274
551
|
setTimeout(msecs, callback) {
|
|
275
552
|
if (callback) this.once('timeout', callback);
|
|
276
553
|
return this;
|
|
@@ -282,12 +559,22 @@ class ServerResponse extends Writable {
|
|
|
282
559
|
// ClientRequest — extends Writable
|
|
283
560
|
//
|
|
284
561
|
// Wraps outgoing HTTP request on the client side.
|
|
285
|
-
//
|
|
286
|
-
//
|
|
562
|
+
// The body flows as it is written, as node:http's does: held here only
|
|
563
|
+
// until a transport takes the request (_attachSink — the first of the
|
|
564
|
+
// protocols tried to be ready to send), then handed to it, with its
|
|
565
|
+
// backpressure (write() false, 'drain'). What was handed is kept, up to
|
|
566
|
+
// REPLAY_MAX, so that a request another protocol must carry after all — the
|
|
567
|
+
// first failed before the server processed it — can be sent again whole.
|
|
287
568
|
//
|
|
288
569
|
// The 'response' event delivers an IncomingMessage.
|
|
289
570
|
// ============================================================
|
|
290
571
|
|
|
572
|
+
// How much of a request body is kept, once handed to a transport, for
|
|
573
|
+
// sending again over another — what a request refused unprocessed (REFUSED_
|
|
574
|
+
// STREAM, H3_REQUEST_REJECTED, past a GOAWAY) or whose first protocol failed
|
|
575
|
+
// may need. Past it, the body streams without a copy.
|
|
576
|
+
var REPLAY_MAX = 64 * 1024;
|
|
577
|
+
|
|
291
578
|
class ClientRequest extends Writable {
|
|
292
579
|
|
|
293
580
|
constructor(options) {
|
|
@@ -309,6 +596,11 @@ class ClientRequest extends Writable {
|
|
|
309
596
|
|
|
310
597
|
options = options || {};
|
|
311
598
|
|
|
599
|
+
// node:http throws these from http.request() itself: the path (no CR, LF,
|
|
600
|
+
// space or other control character — CVE-2018-12116 was a request split
|
|
601
|
+
// through it) and the method (a token).
|
|
602
|
+
validateRequestTarget(options.path, options.method);
|
|
603
|
+
|
|
312
604
|
this.method = (options.method || 'GET').toUpperCase();
|
|
313
605
|
this.path = options.path || '/';
|
|
314
606
|
this.host = options.hostname || options.host || 'localhost';
|
|
@@ -327,8 +619,17 @@ class ClientRequest extends Writable {
|
|
|
327
619
|
for (var name in options.headers) this.setHeader(name, options.headers[name]);
|
|
328
620
|
}
|
|
329
621
|
|
|
330
|
-
//
|
|
622
|
+
// The body: held until a transport takes the request, then streamed to it.
|
|
331
623
|
this._bodyChunks = [];
|
|
624
|
+
this._bodyBuffered = 0;
|
|
625
|
+
this._pendingWriteCb = null; // a write held for room (before a transport)
|
|
626
|
+
this._sink = null; // the transport request carrying the body
|
|
627
|
+
this._sinkWaitCb = null; // a write waiting for the sink's 'drain'
|
|
628
|
+
this._replay = []; // what the sink was handed, while ≤ REPLAY_MAX
|
|
629
|
+
this._replayBytes = 0;
|
|
630
|
+
this._replayable = true;
|
|
631
|
+
this._trailers = null; // addTrailers()
|
|
632
|
+
this._priority = null; // setPriority()
|
|
332
633
|
|
|
333
634
|
// H3 internals — set by unified client after protocol selection
|
|
334
635
|
this._h3 = null;
|
|
@@ -346,11 +647,49 @@ class ClientRequest extends Writable {
|
|
|
346
647
|
|
|
347
648
|
this._timeoutTimer = null;
|
|
348
649
|
this._timeoutMs = 0;
|
|
650
|
+
|
|
651
|
+
// The response, once there is one (node:http's req.res), and how to stop
|
|
652
|
+
// each transport attempt that carries this request (see _destroy).
|
|
653
|
+
this.res = null;
|
|
654
|
+
this._cancels = [];
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// ---- Cancellation ----
|
|
658
|
+
//
|
|
659
|
+
// destroy() / abort() stop the exchange where it is carried, as node:http's
|
|
660
|
+
// do by destroying the socket. Without this they only marked the request:
|
|
661
|
+
// the transport went on, and the response kept downloading in the
|
|
662
|
+
// background. Each transport attempt registers how to stop itself — over
|
|
663
|
+
// HTTP/3 and HTTP/2 that is the request's stream, not the (shared)
|
|
664
|
+
// connection; over HTTP/1.1, the socket, which cannot be reused mid-exchange.
|
|
665
|
+
|
|
666
|
+
_onCancel(fn) {
|
|
667
|
+
if (this.destroyed) { fn(); return; }
|
|
668
|
+
this._cancels.push(fn);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// node:http: destroyed before a response, the request fails with 'socket
|
|
672
|
+
// hang up' (ECONNRESET) unless given its own error.
|
|
673
|
+
destroy(err) {
|
|
674
|
+
if (this.destroyed) return this;
|
|
675
|
+
if (!err && !this.res && !this.aborted) { err = new Error('socket hang up'); err.code = 'ECONNRESET'; }
|
|
676
|
+
return super.destroy(err);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
_destroy(err, cb) {
|
|
680
|
+
var cancels = this._cancels;
|
|
681
|
+
this._cancels = [];
|
|
682
|
+
for (var i = 0; i < cancels.length; i++) { try { cancels[i](); } catch (e) {} }
|
|
683
|
+
if (this._timeoutTimer) { clearTimeout(this._timeoutTimer); this._timeoutTimer = null; }
|
|
684
|
+
if (this.res && !this.res.complete) this.res._abort(err && err.code !== 'ECONNRESET' ? err : null);
|
|
685
|
+
cb(err);
|
|
349
686
|
}
|
|
350
687
|
|
|
351
688
|
// ---- Header management ----
|
|
352
689
|
|
|
353
690
|
setHeader(name, value) {
|
|
691
|
+
validateHeaderName(name);
|
|
692
|
+
validateHeaderValue(name, value);
|
|
354
693
|
var lower = name.toLowerCase();
|
|
355
694
|
this._headers[lower] = value;
|
|
356
695
|
this._headerNames[lower] = name;
|
|
@@ -388,33 +727,143 @@ class ClientRequest extends Writable {
|
|
|
388
727
|
if (Buffer.isBuffer(chunk)) {
|
|
389
728
|
chunk = new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
390
729
|
}
|
|
730
|
+
if (this._sink && this._wholeBody) {
|
|
731
|
+
// end(data) with nothing written before it: the body whole, handed to
|
|
732
|
+
// the transport as its end — sent with a content-length, as node:http
|
|
733
|
+
// sends end(data). Written, then ended, a transport already able to
|
|
734
|
+
// send had begun streaming on the write: no length, and a server that
|
|
735
|
+
// goes by the headers (Express's body-parser) saw no body. It happened
|
|
736
|
+
// whenever the connection was ready at once — a pooled one.
|
|
737
|
+
this._wholeBody = false;
|
|
738
|
+
this._sinkEnded = true;
|
|
739
|
+
this._remember(chunk);
|
|
740
|
+
this._sink.end(chunk, this._trailers);
|
|
741
|
+
callback();
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
this._wholeBody = false;
|
|
745
|
+
if (this._sink) { this._toSink(chunk, callback); return; }
|
|
746
|
+
// No transport yet (still connecting): held — and the writer told to wait
|
|
747
|
+
// past the high-water mark, as a socket not yet connected would.
|
|
391
748
|
this._bodyChunks.push(chunk);
|
|
392
|
-
|
|
749
|
+
this._bodyBuffered += chunk.byteLength;
|
|
750
|
+
if (this._bodyBuffered < this.writableHighWaterMark) callback();
|
|
751
|
+
else this._pendingWriteCb = callback;
|
|
393
752
|
}
|
|
394
753
|
|
|
395
754
|
_final(callback) {
|
|
396
755
|
this.finished = true;
|
|
397
|
-
this.
|
|
756
|
+
if (this._sink && !this._sinkEnded) { this._sinkEnded = true; this._sink.end(null, this._trailers); }
|
|
398
757
|
callback();
|
|
399
758
|
}
|
|
400
759
|
|
|
760
|
+
// node:http's addTrailers(): fields sent after the body (a HEADERS frame
|
|
761
|
+
// over HTTP/2 and HTTP/3; with chunked encoding over HTTP/1.1). An object,
|
|
762
|
+
// or a list of [name, value] pairs.
|
|
763
|
+
addTrailers(headers) {
|
|
764
|
+
var t = this._trailers || (this._trailers = {});
|
|
765
|
+
var pairs = Array.isArray(headers) ? headers : Object.keys(headers || {}).map(function (k) { return [k, headers[k]]; });
|
|
766
|
+
for (var i = 0; i < pairs.length; i++) {
|
|
767
|
+
var name = pairs[i][0], value = pairs[i][1];
|
|
768
|
+
validateHeaderName(name);
|
|
769
|
+
validateHeaderValue(name, value);
|
|
770
|
+
t[String(name).toLowerCase()] = value;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// The request's priority (RFC 9218): { urgency: 0-7 (3), incremental
|
|
775
|
+
// (false) } — the `priority` header while the request has not gone, a
|
|
776
|
+
// PRIORITY_UPDATE after it (HTTP/2, HTTP/3), as res.setPriority is the
|
|
777
|
+
// server's. Normalized the same way.
|
|
778
|
+
setPriority(p) {
|
|
779
|
+
this._priority = responsePriority(p);
|
|
780
|
+
if (this._sink && typeof this._sink.setPriority === 'function') this._sink.setPriority(this._priority);
|
|
781
|
+
return this;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// ---- The transport that carries the body (client.js) ----
|
|
785
|
+
|
|
786
|
+
// `sink` (a transport's request: write → more?, 'drain', end(chunk,
|
|
787
|
+
// trailers)) takes the request: what is held goes to it, and from now on
|
|
788
|
+
// every write.
|
|
789
|
+
_attachSink(sink) {
|
|
790
|
+
var self = this;
|
|
791
|
+
this._sink = sink;
|
|
792
|
+
if (this._priority && typeof sink.setPriority === 'function') sink.setPriority(this._priority);
|
|
793
|
+
sink.on('drain', function () {
|
|
794
|
+
if (self._sink !== sink || !self._sinkWaitCb) return;
|
|
795
|
+
var cb = self._sinkWaitCb; self._sinkWaitCb = null; cb();
|
|
796
|
+
});
|
|
797
|
+
var held = this._bodyChunks, more = true;
|
|
798
|
+
this._bodyChunks = []; this._bodyBuffered = 0;
|
|
799
|
+
this._sinkEnded = false;
|
|
800
|
+
if (this.finished) {
|
|
801
|
+
// Ended before a transport took it: the body is known whole — one
|
|
802
|
+
// end(), so it goes with its length rather than streamed.
|
|
803
|
+
for (var j = 0; j < held.length; j++) this._remember(held[j]);
|
|
804
|
+
var whole = held.length === 0 ? null : held.length === 1 ? held[0] : Buffer.concat(held);
|
|
805
|
+
this._sinkEnded = true;
|
|
806
|
+
sink.end(whole, this._trailers);
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
for (var i = 0; i < held.length; i++) { this._remember(held[i]); more = sink.write(held[i]); }
|
|
810
|
+
if (this._pendingWriteCb) {
|
|
811
|
+
var cb = this._pendingWriteCb; this._pendingWriteCb = null;
|
|
812
|
+
if (more) cb(); else this._sinkWaitCb = cb;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// The transport failed before the server processed the request: the body
|
|
817
|
+
// back here, whole, for the next — or false if it cannot be (more than
|
|
818
|
+
// REPLAY_MAX was handed over: not kept).
|
|
819
|
+
_detachSink() {
|
|
820
|
+
if (!this._replayable) return false;
|
|
821
|
+
this._sink = null;
|
|
822
|
+
this._bodyChunks = this._replay; this._bodyBuffered = this._replayBytes;
|
|
823
|
+
this._replay = []; this._replayBytes = 0;
|
|
824
|
+
if (this._sinkWaitCb) { this._pendingWriteCb = this._sinkWaitCb; this._sinkWaitCb = null; }
|
|
825
|
+
return true;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
_toSink(chunk, callback) {
|
|
829
|
+
this._remember(chunk);
|
|
830
|
+
if (this._sink.write(chunk)) callback();
|
|
831
|
+
else this._sinkWaitCb = callback;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
_remember(chunk) {
|
|
835
|
+
if (!this._replayable) return;
|
|
836
|
+
this._replayBytes += chunk.byteLength;
|
|
837
|
+
if (this._replayBytes > REPLAY_MAX) { this._replayable = false; this._replay = []; this._replayBytes = 0; return; }
|
|
838
|
+
this._replay.push(chunk);
|
|
839
|
+
}
|
|
840
|
+
|
|
401
841
|
// ---- Convenience overrides ----
|
|
402
842
|
|
|
403
843
|
write(chunk, encoding, callback) {
|
|
404
844
|
if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
|
|
845
|
+
this._wroteBody = true;
|
|
405
846
|
return super.write(chunk, encoding, callback);
|
|
406
847
|
}
|
|
407
848
|
|
|
408
849
|
end(data, encoding, callback) {
|
|
409
850
|
if (typeof data === 'function') { callback = data; data = undefined; encoding = undefined; }
|
|
410
851
|
if (typeof encoding === 'function') { callback = encoding; encoding = undefined; }
|
|
411
|
-
if (data != null)
|
|
852
|
+
if (data != null) {
|
|
853
|
+
// Nothing written before: this chunk is the body whole (_write).
|
|
854
|
+
this._wholeBody = !this._wroteBody && !this.writableEnded;
|
|
855
|
+
this.write(data, encoding);
|
|
856
|
+
}
|
|
412
857
|
return super.end(callback);
|
|
413
858
|
}
|
|
414
859
|
|
|
860
|
+
// node:http: 'abort', then 'close' — no error.
|
|
415
861
|
abort() {
|
|
862
|
+
if (this.aborted || this.destroyed) return;
|
|
416
863
|
this.aborted = true;
|
|
417
|
-
|
|
864
|
+
var self = this;
|
|
865
|
+
process.nextTick(function () { self.emit('abort'); });
|
|
866
|
+
this.destroy();
|
|
418
867
|
}
|
|
419
868
|
|
|
420
869
|
setTimeout(msecs, callback) {
|
|
@@ -426,24 +875,6 @@ class ClientRequest extends Writable {
|
|
|
426
875
|
}
|
|
427
876
|
return this;
|
|
428
877
|
}
|
|
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
878
|
}
|
|
448
879
|
|
|
449
880
|
|
|
@@ -452,6 +883,74 @@ class ClientRequest extends Writable {
|
|
|
452
883
|
// Must be a real Stream so Node's eos() / Express don't crash.
|
|
453
884
|
// ============================================================
|
|
454
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Make an Express application (or anything built the same way) run on these
|
|
888
|
+
* streams.
|
|
889
|
+
*
|
|
890
|
+
* Express does not use the req and res it is handed as they are: on every
|
|
891
|
+
* request it swaps their prototype for its own (Object.setPrototypeOf(req,
|
|
892
|
+
* app.request)), and that chain ends in node:http's IncomingMessage and
|
|
893
|
+
* ServerResponse. Every method of these classes is gone after that — a
|
|
894
|
+
* request body arrived through _pushData, which no longer existed, and the
|
|
895
|
+
* server fell over on the first POST. The response survived only because its
|
|
896
|
+
* methods are also bound onto each instance (see the constructor), and even
|
|
897
|
+
* so it ran on node:http's getters for its state.
|
|
898
|
+
*
|
|
899
|
+
* So the chain is re-parented instead, once per application:
|
|
900
|
+
*
|
|
901
|
+
* before app.response → express's response methods → http.ServerResponse
|
|
902
|
+
* after app.response → a copy of those methods → quico ServerResponse
|
|
903
|
+
*
|
|
904
|
+
* Express's methods — req.get, res.json, res.send, res.cookie,
|
|
905
|
+
* express.static — sit on top and call the ones below, which are now ours:
|
|
906
|
+
* the same object, on HTTP/1.1, HTTP/2 and HTTP/3.
|
|
907
|
+
*
|
|
908
|
+
* A COPY, because the object holding Express's methods is shared by every
|
|
909
|
+
* Express application in the process: re-parenting it would move them all,
|
|
910
|
+
* including any served by node:http. The copy belongs to this application
|
|
911
|
+
* alone. A sub-application inherits from its parent's objects when it is
|
|
912
|
+
* mounted, so the root is the only one that needs it.
|
|
913
|
+
*
|
|
914
|
+
* Recognised by shape — a function with `request` and `response` objects —
|
|
915
|
+
* not by name; left alone when already adopted or not that shape. Returns
|
|
916
|
+
* the handler, for chaining.
|
|
917
|
+
*/
|
|
918
|
+
function adoptApp(handler) {
|
|
919
|
+
if (typeof handler !== 'function') return handler;
|
|
920
|
+
reparent(handler.request, IncomingMessage);
|
|
921
|
+
reparent(handler.response, ServerResponse);
|
|
922
|
+
return handler;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// Put `Base` under obj's prototype chain in place of the class it ends in: the
|
|
926
|
+
// objects between obj and the first class prototype (one owning a
|
|
927
|
+
// `constructor`) are copied, the last copy inheriting from Base.prototype.
|
|
928
|
+
function reparent(obj, Base) {
|
|
929
|
+
if (!obj || typeof obj !== 'object' || obj instanceof Base) return;
|
|
930
|
+
var between = [];
|
|
931
|
+
var p = Object.getPrototypeOf(obj);
|
|
932
|
+
while (p && !Object.prototype.hasOwnProperty.call(p, 'constructor')) {
|
|
933
|
+
between.push(p);
|
|
934
|
+
p = Object.getPrototypeOf(p);
|
|
935
|
+
}
|
|
936
|
+
if (!p || p === Object.prototype) return; // not a class chain: not ours to change
|
|
937
|
+
var top = Base.prototype;
|
|
938
|
+
for (var i = between.length - 1; i >= 0; i--) {
|
|
939
|
+
top = Object.create(top, Object.getOwnPropertyDescriptors(between[i]));
|
|
940
|
+
}
|
|
941
|
+
Object.setPrototypeOf(obj, top);
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
// rawHeaders' pairs as node:http's headersDistinct: name → [values].
|
|
945
|
+
function distinctFields(raw) {
|
|
946
|
+
var out = {};
|
|
947
|
+
for (var i = 0; raw && i + 1 < raw.length; i += 2) {
|
|
948
|
+
var n = String(raw[i]).toLowerCase();
|
|
949
|
+
(out[n] || (out[n] = [])).push(String(raw[i + 1]));
|
|
950
|
+
}
|
|
951
|
+
return out;
|
|
952
|
+
}
|
|
953
|
+
|
|
455
954
|
function _makeSocketStub() {
|
|
456
955
|
var sock = new Duplex({
|
|
457
956
|
read(_size) { /* no-op — no data from QUIC socket */ },
|
|
@@ -482,4 +981,4 @@ function _makeSocketStub() {
|
|
|
482
981
|
}
|
|
483
982
|
|
|
484
983
|
|
|
485
|
-
export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub };
|
|
984
|
+
export { IncomingMessage, ServerResponse, ClientRequest, _makeSocketStub, adoptApp };
|