htp2-wrapper 2.2.1
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/LICENSE +21 -0
- package/README.md +459 -0
- package/index.d.ts +141 -0
- package/k4sgv9gy.cjs +1 -0
- package/package.json +87 -0
- package/source/agent.js +796 -0
- package/source/auto.js +225 -0
- package/source/client-request.js +563 -0
- package/source/incoming-message.js +73 -0
- package/source/index.js +50 -0
- package/source/proxies/get-auth-headers.js +17 -0
- package/source/proxies/h1-over-h2.js +90 -0
- package/source/proxies/h2-over-h1.js +48 -0
- package/source/proxies/h2-over-h2.js +32 -0
- package/source/proxies/h2-over-hx.js +40 -0
- package/source/proxies/initialize.js +21 -0
- package/source/proxies/unexpected-status-code-error.js +11 -0
- package/source/utils/calculate-server-name.js +29 -0
- package/source/utils/check-type.js +20 -0
- package/source/utils/delay-async-destroy.js +33 -0
- package/source/utils/errors.js +51 -0
- package/source/utils/is-request-pseudo-header.js +13 -0
- package/source/utils/js-stream-socket.js +8 -0
- package/source/utils/proxy-events.js +7 -0
- package/source/utils/proxy-socket-handler.js +102 -0
- package/source/utils/validate-header-name.js +11 -0
- package/source/utils/validate-header-value.js +17 -0
package/source/agent.js
ADDED
@@ -0,0 +1,796 @@
|
|
1
|
+
'use strict';
|
2
|
+
// See https://github.com/facebook/jest/issues/2549
|
3
|
+
// eslint-disable-next-line node/prefer-global/url
|
4
|
+
const {URL} = require('url');
|
5
|
+
const EventEmitter = require('events');
|
6
|
+
const tls = require('tls');
|
7
|
+
const http2 = require('http2');
|
8
|
+
const QuickLRU = require('quick-lru');
|
9
|
+
const delayAsyncDestroy = require('./utils/delay-async-destroy.js');
|
10
|
+
|
11
|
+
const kCurrentStreamCount = Symbol('currentStreamCount');
|
12
|
+
const kRequest = Symbol('request');
|
13
|
+
const kOriginSet = Symbol('cachedOriginSet');
|
14
|
+
const kGracefullyClosing = Symbol('gracefullyClosing');
|
15
|
+
const kLength = Symbol('length');
|
16
|
+
|
17
|
+
const nameKeys = [
|
18
|
+
// Not an Agent option actually
|
19
|
+
'createConnection',
|
20
|
+
|
21
|
+
// `http2.connect()` options
|
22
|
+
'maxDeflateDynamicTableSize',
|
23
|
+
'maxSettings',
|
24
|
+
'maxSessionMemory',
|
25
|
+
'maxHeaderListPairs',
|
26
|
+
'maxOutstandingPings',
|
27
|
+
'maxReservedRemoteStreams',
|
28
|
+
'maxSendHeaderBlockLength',
|
29
|
+
'paddingStrategy',
|
30
|
+
'peerMaxConcurrentStreams',
|
31
|
+
'settings',
|
32
|
+
|
33
|
+
// `tls.connect()` source options
|
34
|
+
'family',
|
35
|
+
'localAddress',
|
36
|
+
'rejectUnauthorized',
|
37
|
+
|
38
|
+
// `tls.connect()` secure context options
|
39
|
+
'pskCallback',
|
40
|
+
'minDHSize',
|
41
|
+
|
42
|
+
// `tls.connect()` destination options
|
43
|
+
// - `servername` is automatically validated, skip it
|
44
|
+
// - `host` and `port` just describe the destination server,
|
45
|
+
'path',
|
46
|
+
'socket',
|
47
|
+
|
48
|
+
// `tls.createSecureContext()` options
|
49
|
+
'ca',
|
50
|
+
'cert',
|
51
|
+
'sigalgs',
|
52
|
+
'ciphers',
|
53
|
+
'clientCertEngine',
|
54
|
+
'crl',
|
55
|
+
'dhparam',
|
56
|
+
'ecdhCurve',
|
57
|
+
'honorCipherOrder',
|
58
|
+
'key',
|
59
|
+
'privateKeyEngine',
|
60
|
+
'privateKeyIdentifier',
|
61
|
+
'maxVersion',
|
62
|
+
'minVersion',
|
63
|
+
'pfx',
|
64
|
+
'secureOptions',
|
65
|
+
'secureProtocol',
|
66
|
+
'sessionIdContext',
|
67
|
+
'ticketKeys'
|
68
|
+
];
|
69
|
+
|
70
|
+
const getSortedIndex = (array, value, compare) => {
|
71
|
+
let low = 0;
|
72
|
+
let high = array.length;
|
73
|
+
|
74
|
+
while (low < high) {
|
75
|
+
const mid = (low + high) >>> 1;
|
76
|
+
|
77
|
+
if (compare(array[mid], value)) {
|
78
|
+
low = mid + 1;
|
79
|
+
} else {
|
80
|
+
high = mid;
|
81
|
+
}
|
82
|
+
}
|
83
|
+
|
84
|
+
return low;
|
85
|
+
};
|
86
|
+
|
87
|
+
const compareSessions = (a, b) => a.remoteSettings.maxConcurrentStreams > b.remoteSettings.maxConcurrentStreams;
|
88
|
+
|
89
|
+
// See https://tools.ietf.org/html/rfc8336
|
90
|
+
const closeCoveredSessions = (where, session) => {
|
91
|
+
// Clients SHOULD NOT emit new requests on any connection whose Origin
|
92
|
+
// Set is a proper subset of another connection's Origin Set, and they
|
93
|
+
// SHOULD close it once all outstanding requests are satisfied.
|
94
|
+
for (let index = 0; index < where.length; index++) {
|
95
|
+
const coveredSession = where[index];
|
96
|
+
|
97
|
+
if (
|
98
|
+
// Unfortunately `.every()` returns true for an empty array
|
99
|
+
coveredSession[kOriginSet].length > 0
|
100
|
+
|
101
|
+
// The set is a proper subset when its length is less than the other set.
|
102
|
+
&& coveredSession[kOriginSet].length < session[kOriginSet].length
|
103
|
+
|
104
|
+
// And the other set includes all elements of the subset.
|
105
|
+
&& coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
|
106
|
+
|
107
|
+
// Makes sure that the session can handle all requests from the covered session.
|
108
|
+
&& (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
|
109
|
+
) {
|
110
|
+
// This allows pending requests to finish and prevents making new requests.
|
111
|
+
gracefullyClose(coveredSession);
|
112
|
+
}
|
113
|
+
}
|
114
|
+
};
|
115
|
+
|
116
|
+
// This is basically inverted `closeCoveredSessions(...)`.
|
117
|
+
const closeSessionIfCovered = (where, coveredSession) => {
|
118
|
+
for (let index = 0; index < where.length; index++) {
|
119
|
+
const session = where[index];
|
120
|
+
|
121
|
+
if (
|
122
|
+
coveredSession[kOriginSet].length > 0
|
123
|
+
&& coveredSession[kOriginSet].length < session[kOriginSet].length
|
124
|
+
&& coveredSession[kOriginSet].every(origin => session[kOriginSet].includes(origin))
|
125
|
+
&& (coveredSession[kCurrentStreamCount] + session[kCurrentStreamCount]) <= session.remoteSettings.maxConcurrentStreams
|
126
|
+
) {
|
127
|
+
gracefullyClose(coveredSession);
|
128
|
+
|
129
|
+
return true;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
return false;
|
134
|
+
};
|
135
|
+
|
136
|
+
const gracefullyClose = session => {
|
137
|
+
session[kGracefullyClosing] = true;
|
138
|
+
|
139
|
+
if (session[kCurrentStreamCount] === 0) {
|
140
|
+
session.close();
|
141
|
+
}
|
142
|
+
};
|
143
|
+
|
144
|
+
class Agent extends EventEmitter {
|
145
|
+
constructor({timeout = 0, maxSessions = Number.POSITIVE_INFINITY, maxEmptySessions = 10, maxCachedTlsSessions = 100} = {}) {
|
146
|
+
super();
|
147
|
+
|
148
|
+
// SESSIONS[NORMALIZED_OPTIONS] = [];
|
149
|
+
this.sessions = {};
|
150
|
+
|
151
|
+
// The queue for creating new sessions. It looks like this:
|
152
|
+
// QUEUE[NORMALIZED_OPTIONS][NORMALIZED_ORIGIN] = ENTRY_FUNCTION
|
153
|
+
//
|
154
|
+
// It's faster when there are many origins. If there's only one, then QUEUE[`${options}:${origin}`] is faster.
|
155
|
+
// I guess object creation / deletion is causing the slowdown.
|
156
|
+
//
|
157
|
+
// The entry function has `listeners`, `completed` and `destroyed` properties.
|
158
|
+
// `listeners` is an array of objects containing `resolve` and `reject` functions.
|
159
|
+
// `completed` is a boolean. It's set to true after ENTRY_FUNCTION is executed.
|
160
|
+
// `destroyed` is a boolean. If it's set to true, the session will be destroyed if hasn't connected yet.
|
161
|
+
this.queue = {};
|
162
|
+
|
163
|
+
// Each session will use this timeout value.
|
164
|
+
this.timeout = timeout;
|
165
|
+
|
166
|
+
// Max sessions in total
|
167
|
+
this.maxSessions = maxSessions;
|
168
|
+
|
169
|
+
// Max empty sessions in total
|
170
|
+
this.maxEmptySessions = maxEmptySessions;
|
171
|
+
|
172
|
+
this._emptySessionCount = 0;
|
173
|
+
this._sessionCount = 0;
|
174
|
+
|
175
|
+
// We don't support push streams by default.
|
176
|
+
this.settings = {
|
177
|
+
enablePush: false,
|
178
|
+
initialWindowSize: 1024 * 1024 * 32 // 32MB, see https://github.com/nodejs/node/issues/38426
|
179
|
+
};
|
180
|
+
|
181
|
+
// Reusing TLS sessions increases performance.
|
182
|
+
this.tlsSessionCache = new QuickLRU({maxSize: maxCachedTlsSessions});
|
183
|
+
}
|
184
|
+
|
185
|
+
get protocol() {
|
186
|
+
return 'https:';
|
187
|
+
}
|
188
|
+
|
189
|
+
normalizeOptions(options) {
|
190
|
+
let normalized = '';
|
191
|
+
|
192
|
+
for (let index = 0; index < nameKeys.length; index++) {
|
193
|
+
const key = nameKeys[index];
|
194
|
+
|
195
|
+
normalized += ':';
|
196
|
+
|
197
|
+
if (options && options[key] !== undefined) {
|
198
|
+
normalized += options[key];
|
199
|
+
}
|
200
|
+
}
|
201
|
+
|
202
|
+
return normalized;
|
203
|
+
}
|
204
|
+
|
205
|
+
_processQueue() {
|
206
|
+
if (this._sessionCount >= this.maxSessions) {
|
207
|
+
this.closeEmptySessions(this.maxSessions - this._sessionCount + 1);
|
208
|
+
return;
|
209
|
+
}
|
210
|
+
|
211
|
+
// eslint-disable-next-line guard-for-in
|
212
|
+
for (const normalizedOptions in this.queue) {
|
213
|
+
// eslint-disable-next-line guard-for-in
|
214
|
+
for (const normalizedOrigin in this.queue[normalizedOptions]) {
|
215
|
+
const item = this.queue[normalizedOptions][normalizedOrigin];
|
216
|
+
|
217
|
+
// The entry function can be run only once.
|
218
|
+
if (!item.completed) {
|
219
|
+
item.completed = true;
|
220
|
+
|
221
|
+
item();
|
222
|
+
}
|
223
|
+
}
|
224
|
+
}
|
225
|
+
}
|
226
|
+
|
227
|
+
_isBetterSession(thisStreamCount, thatStreamCount) {
|
228
|
+
return thisStreamCount > thatStreamCount;
|
229
|
+
}
|
230
|
+
|
231
|
+
_accept(session, listeners, normalizedOrigin, options) {
|
232
|
+
let index = 0;
|
233
|
+
|
234
|
+
while (index < listeners.length && session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams) {
|
235
|
+
// We assume `resolve(...)` calls `request(...)` *directly*,
|
236
|
+
// otherwise the session will get overloaded.
|
237
|
+
listeners[index].resolve(session);
|
238
|
+
|
239
|
+
index++;
|
240
|
+
}
|
241
|
+
|
242
|
+
listeners.splice(0, index);
|
243
|
+
|
244
|
+
if (listeners.length > 0) {
|
245
|
+
this.getSession(normalizedOrigin, options, listeners);
|
246
|
+
listeners.length = 0;
|
247
|
+
}
|
248
|
+
}
|
249
|
+
|
250
|
+
getSession(origin, options, listeners) {
|
251
|
+
return new Promise((resolve, reject) => {
|
252
|
+
if (Array.isArray(listeners) && listeners.length > 0) {
|
253
|
+
listeners = [...listeners];
|
254
|
+
|
255
|
+
// Resolve the current promise ASAP, we're just moving the listeners.
|
256
|
+
// They will be executed at a different time.
|
257
|
+
resolve();
|
258
|
+
} else {
|
259
|
+
listeners = [{resolve, reject}];
|
260
|
+
}
|
261
|
+
|
262
|
+
try {
|
263
|
+
// Parse origin
|
264
|
+
if (typeof origin === 'string') {
|
265
|
+
origin = new URL(origin);
|
266
|
+
} else if (!(origin instanceof URL)) {
|
267
|
+
throw new TypeError('The `origin` argument needs to be a string or an URL object');
|
268
|
+
}
|
269
|
+
|
270
|
+
if (options) {
|
271
|
+
// Validate servername
|
272
|
+
const {servername} = options;
|
273
|
+
const {hostname} = origin;
|
274
|
+
if (servername && hostname !== servername) {
|
275
|
+
throw new Error(`Origin ${hostname} differs from servername ${servername}`);
|
276
|
+
}
|
277
|
+
}
|
278
|
+
} catch (error) {
|
279
|
+
for (let index = 0; index < listeners.length; index++) {
|
280
|
+
listeners[index].reject(error);
|
281
|
+
}
|
282
|
+
|
283
|
+
return;
|
284
|
+
}
|
285
|
+
|
286
|
+
const normalizedOptions = this.normalizeOptions(options);
|
287
|
+
const normalizedOrigin = origin.origin;
|
288
|
+
|
289
|
+
if (normalizedOptions in this.sessions) {
|
290
|
+
const sessions = this.sessions[normalizedOptions];
|
291
|
+
|
292
|
+
let maxConcurrentStreams = -1;
|
293
|
+
let currentStreamsCount = -1;
|
294
|
+
let optimalSession;
|
295
|
+
|
296
|
+
// We could just do this.sessions[normalizedOptions].find(...) but that isn't optimal.
|
297
|
+
// Additionally, we are looking for session which has biggest current pending streams count.
|
298
|
+
//
|
299
|
+
// |------------| |------------| |------------| |------------|
|
300
|
+
// | Session: A | | Session: B | | Session: C | | Session: D |
|
301
|
+
// | Pending: 5 |-| Pending: 8 |-| Pending: 9 |-| Pending: 4 |
|
302
|
+
// | Max: 10 | | Max: 10 | | Max: 9 | | Max: 5 |
|
303
|
+
// |------------| |------------| |------------| |------------|
|
304
|
+
// ^
|
305
|
+
// |
|
306
|
+
// pick this one --
|
307
|
+
//
|
308
|
+
for (let index = 0; index < sessions.length; index++) {
|
309
|
+
const session = sessions[index];
|
310
|
+
|
311
|
+
const sessionMaxConcurrentStreams = session.remoteSettings.maxConcurrentStreams;
|
312
|
+
|
313
|
+
if (sessionMaxConcurrentStreams < maxConcurrentStreams) {
|
314
|
+
break;
|
315
|
+
}
|
316
|
+
|
317
|
+
if (!session[kOriginSet].includes(normalizedOrigin)) {
|
318
|
+
continue;
|
319
|
+
}
|
320
|
+
|
321
|
+
const sessionCurrentStreamsCount = session[kCurrentStreamCount];
|
322
|
+
|
323
|
+
if (
|
324
|
+
sessionCurrentStreamsCount >= sessionMaxConcurrentStreams
|
325
|
+
|| session[kGracefullyClosing]
|
326
|
+
// Unfortunately the `close` event isn't called immediately,
|
327
|
+
// so `session.destroyed` is `true`, but `session.closed` is `false`.
|
328
|
+
|| session.destroyed
|
329
|
+
) {
|
330
|
+
continue;
|
331
|
+
}
|
332
|
+
|
333
|
+
// We only need set this once.
|
334
|
+
if (!optimalSession) {
|
335
|
+
maxConcurrentStreams = sessionMaxConcurrentStreams;
|
336
|
+
}
|
337
|
+
|
338
|
+
// Either get the session which has biggest current stream count or the lowest.
|
339
|
+
if (this._isBetterSession(sessionCurrentStreamsCount, currentStreamsCount)) {
|
340
|
+
optimalSession = session;
|
341
|
+
currentStreamsCount = sessionCurrentStreamsCount;
|
342
|
+
}
|
343
|
+
}
|
344
|
+
|
345
|
+
if (optimalSession) {
|
346
|
+
this._accept(optimalSession, listeners, normalizedOrigin, options);
|
347
|
+
return;
|
348
|
+
}
|
349
|
+
}
|
350
|
+
|
351
|
+
if (normalizedOptions in this.queue) {
|
352
|
+
if (normalizedOrigin in this.queue[normalizedOptions]) {
|
353
|
+
// There's already an item in the queue, just attach ourselves to it.
|
354
|
+
this.queue[normalizedOptions][normalizedOrigin].listeners.push(...listeners);
|
355
|
+
return;
|
356
|
+
}
|
357
|
+
} else {
|
358
|
+
this.queue[normalizedOptions] = {
|
359
|
+
[kLength]: 0
|
360
|
+
};
|
361
|
+
}
|
362
|
+
|
363
|
+
// The entry must be removed from the queue IMMEDIATELY when:
|
364
|
+
// 1. the session connects successfully,
|
365
|
+
// 2. an error occurs.
|
366
|
+
const removeFromQueue = () => {
|
367
|
+
// Our entry can be replaced. We cannot remove the new one.
|
368
|
+
if (normalizedOptions in this.queue && this.queue[normalizedOptions][normalizedOrigin] === entry) {
|
369
|
+
delete this.queue[normalizedOptions][normalizedOrigin];
|
370
|
+
|
371
|
+
if (--this.queue[normalizedOptions][kLength] === 0) {
|
372
|
+
delete this.queue[normalizedOptions];
|
373
|
+
}
|
374
|
+
}
|
375
|
+
};
|
376
|
+
|
377
|
+
// The main logic is here
|
378
|
+
const entry = async () => {
|
379
|
+
this._sessionCount++;
|
380
|
+
|
381
|
+
const name = `${normalizedOrigin}:${normalizedOptions}`;
|
382
|
+
let receivedSettings = false;
|
383
|
+
let socket;
|
384
|
+
|
385
|
+
try {
|
386
|
+
const computedOptions = {...options};
|
387
|
+
|
388
|
+
if (computedOptions.settings === undefined) {
|
389
|
+
computedOptions.settings = this.settings;
|
390
|
+
}
|
391
|
+
|
392
|
+
if (computedOptions.session === undefined) {
|
393
|
+
computedOptions.session = this.tlsSessionCache.get(name);
|
394
|
+
}
|
395
|
+
|
396
|
+
const createConnection = computedOptions.createConnection || this.createConnection;
|
397
|
+
|
398
|
+
// A hacky workaround to enable async `createConnection`
|
399
|
+
socket = await createConnection.call(this, origin, computedOptions);
|
400
|
+
computedOptions.createConnection = () => socket;
|
401
|
+
|
402
|
+
const session = http2.connect(origin, computedOptions);
|
403
|
+
session[kCurrentStreamCount] = 0;
|
404
|
+
session[kGracefullyClosing] = false;
|
405
|
+
|
406
|
+
// Node.js return https://false:443 instead of https://1.1.1.1:443
|
407
|
+
const getOriginSet = () => {
|
408
|
+
const {socket} = session;
|
409
|
+
|
410
|
+
let originSet;
|
411
|
+
if (socket.servername === false) {
|
412
|
+
socket.servername = socket.remoteAddress;
|
413
|
+
originSet = session.originSet;
|
414
|
+
socket.servername = false;
|
415
|
+
} else {
|
416
|
+
originSet = session.originSet;
|
417
|
+
}
|
418
|
+
|
419
|
+
return originSet;
|
420
|
+
};
|
421
|
+
|
422
|
+
const isFree = () => session[kCurrentStreamCount] < session.remoteSettings.maxConcurrentStreams;
|
423
|
+
|
424
|
+
session.socket.once('session', tlsSession => {
|
425
|
+
this.tlsSessionCache.set(name, tlsSession);
|
426
|
+
});
|
427
|
+
|
428
|
+
session.once('error', error => {
|
429
|
+
// Listeners are empty when the session successfully connected.
|
430
|
+
for (let index = 0; index < listeners.length; index++) {
|
431
|
+
listeners[index].reject(error);
|
432
|
+
}
|
433
|
+
|
434
|
+
// The connection got broken, purge the cache.
|
435
|
+
this.tlsSessionCache.delete(name);
|
436
|
+
});
|
437
|
+
|
438
|
+
session.setTimeout(this.timeout, () => {
|
439
|
+
// Terminates all streams owned by this session.
|
440
|
+
session.destroy();
|
441
|
+
});
|
442
|
+
|
443
|
+
session.once('close', () => {
|
444
|
+
this._sessionCount--;
|
445
|
+
|
446
|
+
if (receivedSettings) {
|
447
|
+
// Assumes session `close` is emitted after request `close`
|
448
|
+
this._emptySessionCount--;
|
449
|
+
|
450
|
+
// This cannot be moved to the stream logic,
|
451
|
+
// because there may be a session that hadn't made a single request.
|
452
|
+
const where = this.sessions[normalizedOptions];
|
453
|
+
|
454
|
+
if (where.length === 1) {
|
455
|
+
delete this.sessions[normalizedOptions];
|
456
|
+
} else {
|
457
|
+
where.splice(where.indexOf(session), 1);
|
458
|
+
}
|
459
|
+
} else {
|
460
|
+
// Broken connection
|
461
|
+
removeFromQueue();
|
462
|
+
|
463
|
+
const error = new Error('Session closed without receiving a SETTINGS frame');
|
464
|
+
error.code = 'HTTP2WRAPPER_NOSETTINGS';
|
465
|
+
|
466
|
+
for (let index = 0; index < listeners.length; index++) {
|
467
|
+
listeners[index].reject(error);
|
468
|
+
}
|
469
|
+
}
|
470
|
+
|
471
|
+
// There may be another session awaiting.
|
472
|
+
this._processQueue();
|
473
|
+
});
|
474
|
+
|
475
|
+
// Iterates over the queue and processes listeners.
|
476
|
+
const processListeners = () => {
|
477
|
+
const queue = this.queue[normalizedOptions];
|
478
|
+
if (!queue) {
|
479
|
+
return;
|
480
|
+
}
|
481
|
+
|
482
|
+
const originSet = session[kOriginSet];
|
483
|
+
|
484
|
+
for (let index = 0; index < originSet.length; index++) {
|
485
|
+
const origin = originSet[index];
|
486
|
+
|
487
|
+
if (origin in queue) {
|
488
|
+
const {listeners, completed} = queue[origin];
|
489
|
+
|
490
|
+
let index = 0;
|
491
|
+
|
492
|
+
// Prevents session overloading.
|
493
|
+
while (index < listeners.length && isFree()) {
|
494
|
+
// We assume `resolve(...)` calls `request(...)` *directly*,
|
495
|
+
// otherwise the session will get overloaded.
|
496
|
+
listeners[index].resolve(session);
|
497
|
+
|
498
|
+
index++;
|
499
|
+
}
|
500
|
+
|
501
|
+
queue[origin].listeners.splice(0, index);
|
502
|
+
|
503
|
+
if (queue[origin].listeners.length === 0 && !completed) {
|
504
|
+
delete queue[origin];
|
505
|
+
|
506
|
+
if (--queue[kLength] === 0) {
|
507
|
+
delete this.queue[normalizedOptions];
|
508
|
+
break;
|
509
|
+
}
|
510
|
+
}
|
511
|
+
|
512
|
+
// We're no longer free, no point in continuing.
|
513
|
+
if (!isFree()) {
|
514
|
+
break;
|
515
|
+
}
|
516
|
+
}
|
517
|
+
}
|
518
|
+
};
|
519
|
+
|
520
|
+
// The Origin Set cannot shrink. No need to check if it suddenly became covered by another one.
|
521
|
+
session.on('origin', () => {
|
522
|
+
session[kOriginSet] = getOriginSet() || [];
|
523
|
+
session[kGracefullyClosing] = false;
|
524
|
+
closeSessionIfCovered(this.sessions[normalizedOptions], session);
|
525
|
+
|
526
|
+
if (session[kGracefullyClosing] || !isFree()) {
|
527
|
+
return;
|
528
|
+
}
|
529
|
+
|
530
|
+
processListeners();
|
531
|
+
|
532
|
+
if (!isFree()) {
|
533
|
+
return;
|
534
|
+
}
|
535
|
+
|
536
|
+
// Close covered sessions (if possible).
|
537
|
+
closeCoveredSessions(this.sessions[normalizedOptions], session);
|
538
|
+
});
|
539
|
+
|
540
|
+
session.once('remoteSettings', () => {
|
541
|
+
// The Agent could have been destroyed already.
|
542
|
+
if (entry.destroyed) {
|
543
|
+
const error = new Error('Agent has been destroyed');
|
544
|
+
|
545
|
+
for (let index = 0; index < listeners.length; index++) {
|
546
|
+
listeners[index].reject(error);
|
547
|
+
}
|
548
|
+
|
549
|
+
session.destroy();
|
550
|
+
return;
|
551
|
+
}
|
552
|
+
|
553
|
+
// See https://github.com/nodejs/node/issues/38426
|
554
|
+
if (session.setLocalWindowSize) {
|
555
|
+
session.setLocalWindowSize(1024 * 1024 * 4); // 4 MB
|
556
|
+
}
|
557
|
+
|
558
|
+
session[kOriginSet] = getOriginSet() || [];
|
559
|
+
|
560
|
+
if (session.socket.encrypted) {
|
561
|
+
const mainOrigin = session[kOriginSet][0];
|
562
|
+
if (mainOrigin !== normalizedOrigin) {
|
563
|
+
const error = new Error(`Requested origin ${normalizedOrigin} does not match server ${mainOrigin}`);
|
564
|
+
|
565
|
+
for (let index = 0; index < listeners.length; index++) {
|
566
|
+
listeners[index].reject(error);
|
567
|
+
}
|
568
|
+
|
569
|
+
session.destroy();
|
570
|
+
return;
|
571
|
+
}
|
572
|
+
}
|
573
|
+
|
574
|
+
removeFromQueue();
|
575
|
+
|
576
|
+
{
|
577
|
+
const where = this.sessions;
|
578
|
+
|
579
|
+
if (normalizedOptions in where) {
|
580
|
+
const sessions = where[normalizedOptions];
|
581
|
+
sessions.splice(getSortedIndex(sessions, session, compareSessions), 0, session);
|
582
|
+
} else {
|
583
|
+
where[normalizedOptions] = [session];
|
584
|
+
}
|
585
|
+
}
|
586
|
+
|
587
|
+
receivedSettings = true;
|
588
|
+
this._emptySessionCount++;
|
589
|
+
|
590
|
+
this.emit('session', session);
|
591
|
+
this._accept(session, listeners, normalizedOrigin, options);
|
592
|
+
|
593
|
+
if (session[kCurrentStreamCount] === 0 && this._emptySessionCount > this.maxEmptySessions) {
|
594
|
+
this.closeEmptySessions(this._emptySessionCount - this.maxEmptySessions);
|
595
|
+
}
|
596
|
+
|
597
|
+
// `session.remoteSettings.maxConcurrentStreams` might get increased
|
598
|
+
session.on('remoteSettings', () => {
|
599
|
+
if (!isFree()) {
|
600
|
+
return;
|
601
|
+
}
|
602
|
+
|
603
|
+
processListeners();
|
604
|
+
|
605
|
+
if (!isFree()) {
|
606
|
+
return;
|
607
|
+
}
|
608
|
+
|
609
|
+
// In case the Origin Set changes
|
610
|
+
closeCoveredSessions(this.sessions[normalizedOptions], session);
|
611
|
+
});
|
612
|
+
});
|
613
|
+
|
614
|
+
// Shim `session.request()` in order to catch all streams
|
615
|
+
session[kRequest] = session.request;
|
616
|
+
session.request = (headers, streamOptions) => {
|
617
|
+
if (session[kGracefullyClosing]) {
|
618
|
+
throw new Error('The session is gracefully closing. No new streams are allowed.');
|
619
|
+
}
|
620
|
+
|
621
|
+
const stream = session[kRequest](headers, streamOptions);
|
622
|
+
|
623
|
+
// The process won't exit until the session is closed or all requests are gone.
|
624
|
+
session.ref();
|
625
|
+
|
626
|
+
if (session[kCurrentStreamCount]++ === 0) {
|
627
|
+
this._emptySessionCount--;
|
628
|
+
}
|
629
|
+
|
630
|
+
stream.once('close', () => {
|
631
|
+
if (--session[kCurrentStreamCount] === 0) {
|
632
|
+
this._emptySessionCount++;
|
633
|
+
session.unref();
|
634
|
+
|
635
|
+
if (this._emptySessionCount > this.maxEmptySessions || session[kGracefullyClosing]) {
|
636
|
+
session.close();
|
637
|
+
return;
|
638
|
+
}
|
639
|
+
}
|
640
|
+
|
641
|
+
if (session.destroyed || session.closed) {
|
642
|
+
return;
|
643
|
+
}
|
644
|
+
|
645
|
+
if (isFree() && !closeSessionIfCovered(this.sessions[normalizedOptions], session)) {
|
646
|
+
closeCoveredSessions(this.sessions[normalizedOptions], session);
|
647
|
+
processListeners();
|
648
|
+
|
649
|
+
if (session[kCurrentStreamCount] === 0) {
|
650
|
+
this._processQueue();
|
651
|
+
}
|
652
|
+
}
|
653
|
+
});
|
654
|
+
|
655
|
+
return stream;
|
656
|
+
};
|
657
|
+
} catch (error) {
|
658
|
+
removeFromQueue();
|
659
|
+
this._sessionCount--;
|
660
|
+
|
661
|
+
for (let index = 0; index < listeners.length; index++) {
|
662
|
+
listeners[index].reject(error);
|
663
|
+
}
|
664
|
+
}
|
665
|
+
};
|
666
|
+
|
667
|
+
entry.listeners = listeners;
|
668
|
+
entry.completed = false;
|
669
|
+
entry.destroyed = false;
|
670
|
+
|
671
|
+
this.queue[normalizedOptions][normalizedOrigin] = entry;
|
672
|
+
this.queue[normalizedOptions][kLength]++;
|
673
|
+
this._processQueue();
|
674
|
+
});
|
675
|
+
}
|
676
|
+
|
677
|
+
request(origin, options, headers, streamOptions) {
|
678
|
+
return new Promise((resolve, reject) => {
|
679
|
+
this.getSession(origin, options, [{
|
680
|
+
reject,
|
681
|
+
resolve: session => {
|
682
|
+
try {
|
683
|
+
const stream = session.request(headers, streamOptions);
|
684
|
+
|
685
|
+
// Do not throw before `request(...)` has been awaited
|
686
|
+
delayAsyncDestroy(stream);
|
687
|
+
|
688
|
+
resolve(stream);
|
689
|
+
} catch (error) {
|
690
|
+
reject(error);
|
691
|
+
}
|
692
|
+
}
|
693
|
+
}]);
|
694
|
+
});
|
695
|
+
}
|
696
|
+
|
697
|
+
async createConnection(origin, options) {
|
698
|
+
return Agent.connect(origin, options);
|
699
|
+
}
|
700
|
+
|
701
|
+
static connect(origin, options) {
|
702
|
+
options.ALPNProtocols = ['h2'];
|
703
|
+
|
704
|
+
const port = origin.port || 443;
|
705
|
+
const host = origin.hostname;
|
706
|
+
|
707
|
+
if (typeof options.servername === 'undefined') {
|
708
|
+
options.servername = host;
|
709
|
+
}
|
710
|
+
|
711
|
+
const socket = tls.connect(port, host, options);
|
712
|
+
|
713
|
+
if (options.socket) {
|
714
|
+
socket._peername = {
|
715
|
+
family: undefined,
|
716
|
+
address: undefined,
|
717
|
+
port
|
718
|
+
};
|
719
|
+
}
|
720
|
+
|
721
|
+
return socket;
|
722
|
+
}
|
723
|
+
|
724
|
+
closeEmptySessions(maxCount = Number.POSITIVE_INFINITY) {
|
725
|
+
let closedCount = 0;
|
726
|
+
|
727
|
+
const {sessions} = this;
|
728
|
+
|
729
|
+
// eslint-disable-next-line guard-for-in
|
730
|
+
for (const key in sessions) {
|
731
|
+
const thisSessions = sessions[key];
|
732
|
+
|
733
|
+
for (let index = 0; index < thisSessions.length; index++) {
|
734
|
+
const session = thisSessions[index];
|
735
|
+
|
736
|
+
if (session[kCurrentStreamCount] === 0) {
|
737
|
+
closedCount++;
|
738
|
+
session.close();
|
739
|
+
|
740
|
+
if (closedCount >= maxCount) {
|
741
|
+
return closedCount;
|
742
|
+
}
|
743
|
+
}
|
744
|
+
}
|
745
|
+
}
|
746
|
+
|
747
|
+
return closedCount;
|
748
|
+
}
|
749
|
+
|
750
|
+
destroy(reason) {
|
751
|
+
const {sessions, queue} = this;
|
752
|
+
|
753
|
+
// eslint-disable-next-line guard-for-in
|
754
|
+
for (const key in sessions) {
|
755
|
+
const thisSessions = sessions[key];
|
756
|
+
|
757
|
+
for (let index = 0; index < thisSessions.length; index++) {
|
758
|
+
thisSessions[index].destroy(reason);
|
759
|
+
}
|
760
|
+
}
|
761
|
+
|
762
|
+
// eslint-disable-next-line guard-for-in
|
763
|
+
for (const normalizedOptions in queue) {
|
764
|
+
const entries = queue[normalizedOptions];
|
765
|
+
|
766
|
+
// eslint-disable-next-line guard-for-in
|
767
|
+
for (const normalizedOrigin in entries) {
|
768
|
+
entries[normalizedOrigin].destroyed = true;
|
769
|
+
}
|
770
|
+
}
|
771
|
+
|
772
|
+
// New requests should NOT attach to destroyed sessions
|
773
|
+
this.queue = {};
|
774
|
+
this.tlsSessionCache.clear();
|
775
|
+
}
|
776
|
+
|
777
|
+
get emptySessionCount() {
|
778
|
+
return this._emptySessionCount;
|
779
|
+
}
|
780
|
+
|
781
|
+
get pendingSessionCount() {
|
782
|
+
return this._sessionCount - this._emptySessionCount;
|
783
|
+
}
|
784
|
+
|
785
|
+
get sessionCount() {
|
786
|
+
return this._sessionCount;
|
787
|
+
}
|
788
|
+
}
|
789
|
+
|
790
|
+
Agent.kCurrentStreamCount = kCurrentStreamCount;
|
791
|
+
Agent.kGracefullyClosing = kGracefullyClosing;
|
792
|
+
|
793
|
+
module.exports = {
|
794
|
+
Agent,
|
795
|
+
globalAgent: new Agent()
|
796
|
+
};
|